Skip to content

Commit 87d004a

Browse files
working-ish population of R&C profile from SAM economics
1 parent 31c134b commit 87d004a

File tree

3 files changed

+106
-6
lines changed

3 files changed

+106
-6
lines changed

src/geophires_x/Economics.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import numpy as np
55
import numpy_financial as npf
66
import geophires_x.Model as Model
7-
from geophires_x.EconomicsSam import calculate_sam_economics
7+
from geophires_x.EconomicsSam import calculate_sam_economics, calculate_sam_economics_cashflow
88
from geophires_x.OptionList import Configuration, WellDrillingCostCorrelation, EconomicModel, EndUseOptions, PlantType, \
99
_WellDrillingCostCorrelationCitation
1010
from geophires_x.Parameter import intParameter, floatParameter, OutputParameter, ReadParameter, boolParameter, \
@@ -2830,6 +2830,10 @@ def calculate_cashflow(self, model: Model) -> None:
28302830
self.TotalCummRevenue.value = [0.0] * total_duration
28312831
self.CarbonThatWouldHaveBeenProducedTotal.value = 0.0
28322832

2833+
if model.economics.econmodel.value == EconomicModel.SAM_SINGLE_OWNER_PPA:
2834+
calculate_sam_economics_cashflow(model)
2835+
return
2836+
28332837
# Based on the style of the project, calculate the revenue & cumulative revenue
28342838
if model.surfaceplant.enduse_option.value == EndUseOptions.ELECTRICITY:
28352839
self.ElecRevenue.value, self.ElecCummRevenue.value = CalculateRevenue(

src/geophires_x/EconomicsSam.py

Lines changed: 94 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import PySAM.Utilityrate5 as UtilityRate
2323

2424
import geophires_x.Model as Model
25+
from geophires_x.OptionList import EndUseOptions
2526

2627

2728
@lru_cache(maxsize=12)
@@ -57,6 +58,13 @@ def calculate_sam_economics(
5758
('IRR', single_owner.Outputs.project_return_aftertax_irr, '%'),
5859
('NPV', single_owner.Outputs.project_return_aftertax_npv * 1e-6, 'MUSD'),
5960
('CAPEX', single_owner.Outputs.adjusted_installed_cost * 1e-6, 'MUSD'),
61+
62+
('Electricity Price (cents/kWh)', [p * 1e-2 for p in single_owner.Outputs.cf_ppa_price], 'cents/kWh'),
63+
('Electricity Ann. Rev. (MUSD/yr)', [e * 1e-6 for e in single_owner.Outputs.cf_energy_value], '(MUSD/yr)'),
64+
65+
# TODO determine if this is the 'appropriate' cashflow variable
66+
('Project Net Rev (MUSD/yr)', [c * 1e-6 for c in single_owner.Outputs.cf_pretax_cashflow], 'MUSD/yr'),
67+
6068
# ('Gross Output', gt.Outputs.gross_output, 'MW'),
6169
# ('Net Output', gt.Outputs.gross_output - gt.Outputs.pump_work, 'MW')
6270
]
@@ -110,7 +118,7 @@ def _get_single_owner_parameters(model: Model) -> dict[str, Any]:
110118

111119

112120
@lru_cache(maxsize=12)
113-
def _get_revenue_and_cashflow_profile(model: Model):
121+
def calculate_sam_economics_cashflow(model: Model):
114122
"""
115123
ENERGY
116124
Electricity Provided -> cf_energy_sales
@@ -126,12 +134,95 @@ def _get_revenue_and_cashflow_profile(model: Model):
126134
Net Revenue -> cf_total_revenue
127135
"""
128136

137+
econ = model.economics
138+
sam_econ = calculate_sam_economics(model)
139+
140+
construction_years = model.surfaceplant.construction_years.value
141+
plant_lifetime = model.surfaceplant.plant_lifetime.value
142+
total_duration = plant_lifetime + construction_years
143+
144+
# econ.ElecRevenue.value = [0.0] * total_duration
145+
# econ.ElecCummRevenue.value = [0.0] * total_duration
146+
# econ.HeatRevenue.value = [0.0] * total_duration
147+
# econ.HeatCummRevenue.value = [0.0] * total_duration
148+
# econ.CoolingRevenue.value = [0.0] * total_duration
149+
# econ.CoolingCummRevenue.value = [0.0] * total_duration
150+
# econ.CarbonRevenue.value = [0.0] * total_duration
151+
# econ.CarbonCummCashFlow.value = [0.0] * total_duration
152+
# econ.TotalRevenue.value = [0.0] * total_duration
153+
# econ.TotalCummRevenue.value = [0.0] * total_duration
154+
# econ.CarbonThatWouldHaveBeenProducedTotal.value = 0.0
155+
156+
def _cumm(cash_flow: list) -> list:
157+
cumm_cash_flow = [0.0] * total_duration
158+
for i in range(construction_years, total_duration, 1):
159+
cumm_cash_flow[i] = cumm_cash_flow[i - 1] + cash_flow[i]
160+
161+
return cumm_cash_flow
162+
163+
# Based on the style of the project, calculate the revenue & cumulative revenue
164+
if model.surfaceplant.enduse_option.value == EndUseOptions.ELECTRICITY:
165+
econ.ElecPrice.value = sam_econ['Electricity Price (cents/kWh)']['value'].copy()
166+
econ.ElecRevenue.value = sam_econ['Electricity Ann. Rev. (MUSD/yr)']['value'].copy() # FIXME WIP
167+
econ.ElecCummRevenue.value = _cumm(econ.ElecRevenue.value)
168+
169+
# econ.ElecRevenue.value, econ.ElecCummRevenue.value = CalculateRevenue(
170+
# model.surfaceplant.plant_lifetime.value, model.surfaceplant.construction_years.value,
171+
# model.surfaceplant.NetkWhProduced.value, econ.ElecPrice.value)
172+
# econ.TotalRevenue.value = econ.ElecRevenue.value.copy()
173+
# econ.TotalCummRevenue.value = econ.ElecCummRevenue.value
174+
else:
175+
raise ValueError(f'Unexpected End-Use Option: {model.surfaceplant.enduse_option.value}')
176+
177+
if econ.DoCarbonCalculations.value:
178+
raise NotImplementedError
179+
180+
# FIXME TODO
181+
# econ.CarbonRevenue.value, econ.CarbonCummCashFlow.value, econ.CarbonThatWouldHaveBeenProducedAnnually.value, \
182+
# econ.CarbonThatWouldHaveBeenProducedTotal.value = econ.CalculateCarbonRevenue(model,
183+
# model.surfaceplant.plant_lifetime.value, model.surfaceplant.construction_years.value,
184+
# econ.CarbonPrice.value, econ.GridCO2Intensity.value, econ.NaturalGasCO2Intensity.value,
185+
# model.surfaceplant.NetkWhProduced.value, model.surfaceplant.HeatkWhProduced.value)
186+
# for i in range(model.surfaceplant.construction_years.value, model.surfaceplant.plant_lifetime.value + model.surfaceplant.construction_years.value, 1):
187+
# econ.TotalRevenue.value[i] = econ.TotalRevenue.value[i] + econ.CarbonRevenue.value[i]
188+
# #econ.TotalCummRevenue.value[i] = econ.TotalCummRevenue.value[i] + econ.CarbonCummCashFlow.value[i]
189+
190+
# FIXME WIP TODO pass/reconcile non-1 construction years in/from SAM
191+
# for the sake of display, insert zeros at the beginning of the pricing arrays
192+
for i in range(0, model.surfaceplant.construction_years.value, 1):
193+
# econ.ElecPrice.value.insert(0, 0.0)
194+
econ.HeatPrice.value.insert(0, 0.0)
195+
econ.CoolingPrice.value.insert(0, 0.0)
196+
econ.CarbonPrice.value.insert(0, 0.0)
197+
198+
# Insert the cost of construction into the front of the array that will be used to calculate NPV
199+
# the convention is that the upfront CAPEX is negative
200+
# This is the same for all projects
201+
# ProjectCAPEXPerConstructionYear = econ.CCap.value / model.surfaceplant.construction_years.value
202+
# for i in range(0, model.surfaceplant.construction_years.value, 1):
203+
# econ.TotalRevenue.value[i] = -1.0 * ProjectCAPEXPerConstructionYear
204+
# econ.TotalCummRevenue.value[i] = -1.0 * ProjectCAPEXPerConstructionYear
205+
# econ.TotalRevenue.value, econ.TotalCummRevenue.value = CalculateTotalRevenue(
206+
# model.surfaceplant.plant_lifetime.value, model.surfaceplant.construction_years.value, econ.CCap.value,
207+
# econ.Coam.value, econ.TotalRevenue.value, econ.TotalCummRevenue.value)
208+
209+
econ.TotalRevenue.value = sam_econ['Project Net Rev (MUSD/yr)']['value'].copy()
210+
211+
econ.TotalCummRevenue.value = _cumm(econ.TotalRevenue.value)
212+
213+
129214
def _get_average_net_generation_MW(model: Model) -> float:
130215
return np.average(model.surfaceplant.NetElectricityProduced.value)
131216

132217

133-
def _sig_figs(val: float, num_sig_figs: int) -> float:
218+
def _sig_figs(val: float | list[float] | tuple[float], num_sig_figs: int) -> float:
134219
if val is None:
135220
return None
136221

137-
return float('%s' % float(f'%.{num_sig_figs}g' % val)) # pylint: disable=consider-using-f-string
222+
if isinstance(val, list) or isinstance(val, tuple):
223+
return [_sig_figs(v, num_sig_figs) for v in val]
224+
225+
try:
226+
return float('%s' % float(f'%.{num_sig_figs}g' % val)) # pylint: disable=consider-using-f-string
227+
except TypeError as te:
228+
raise RuntimeError from te

tests/geophires_x_tests/test_economics_sam.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from base_test_case import BaseTestCase
2+
from geophires_x.EconomicsSam import _sig_figs
23
from geophires_x_client import GeophiresInputParameters
34
from geophires_x_client import GeophiresXClient
45
from geophires_x_client import GeophiresXResult
@@ -9,8 +10,8 @@ class EconomicsSamTestCase(BaseTestCase):
910
def _get_result(self, _params) -> GeophiresXResult:
1011
return GeophiresXClient().get_geophires_result(
1112
GeophiresInputParameters(
12-
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
13+
# from_file_path=self._get_test_file_path('generic-egs-case.txt'),
14+
from_file_path=self._get_test_file_path('../examples/Fervo_Project_Cape-3.txt'), # FIXME TEMP
1415
params={'Economic Model': 5, **_params},
1516
)
1617
)
@@ -33,3 +34,7 @@ def _npv(r: GeophiresXResult) -> float:
3334
def test_only_electricity_end_use_supported(self):
3435
with self.assertRaises(RuntimeError):
3536
self._get_result({'End-Use Option': 2})
37+
38+
def test_sig_figs(self):
39+
self.assertListEqual(_sig_figs([1.14, 2.24], 2), [1.1, 2.2])
40+
self.assertListEqual(_sig_figs((1.14, 2.24), 2), [1.1, 2.2])

0 commit comments

Comments
 (0)