Skip to content

Commit 3441053

Browse files
finish integrating capacity/utilization factor, including resolution of a few incidental nits discovered in the course of implementation
1 parent a55fbcc commit 3441053

File tree

4 files changed

+64
-18
lines changed

4 files changed

+64
-18
lines changed

src/geophires_x/EconomicsSam.py

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ def calculate_sam_economics(model: Model) -> dict[str, dict[str, Any]]:
8484
module.value(k, v)
8585

8686
for k, v in _get_custom_gen_parameters(model).items():
87-
single_owner.value(k, v)
87+
custom_gen.value(k, v)
8888

8989
for k, v in _get_utility_rate_parameters(model).items():
9090
single_owner.value(k, v)
@@ -151,7 +151,8 @@ def _get_custom_gen_parameters(model: Model) -> dict[str, Any]:
151151
# fmt:off
152152
ret: dict[str, Any] = {
153153
# Project lifetime
154-
'analysis_period': model.surfaceplant.plant_lifetime.value
154+
'analysis_period': model.surfaceplant.plant_lifetime.value,
155+
'user_capacity_factor': _pct(model.surfaceplant.utilization_factor),
155156
}
156157
# fmt:on
157158

@@ -178,8 +179,7 @@ def _get_single_owner_parameters(model: Model) -> dict[str, Any]:
178179

179180
ret: dict[str, Any] = {}
180181

181-
def pct(econ_value: Parameter) -> float:
182-
return econ_value.quantity().to(convertible_unit('%')).magnitude
182+
ret['analysis_period'] = model.surfaceplant.plant_lifetime.value
183183

184184
itc = econ.RITCValue.value
185185
total_capex_musd = econ.CCap.value + itc
@@ -189,7 +189,7 @@ def pct(econ_value: Parameter) -> float:
189189
opex_musd = econ.Coam.value
190190
ret['om_fixed'] = [opex_musd * 1e6]
191191
# GEOPHIRES assumes O&M fixed costs are not affected by inflation
192-
ret['om_fixed_escal'] = -1.0 * pct(econ.RINFL)
192+
ret['om_fixed_escal'] = -1.0 * _pct(econ.RINFL)
193193

194194
# TODO construction years
195195

@@ -198,8 +198,6 @@ def pct(econ_value: Parameter) -> float:
198198
# Note generation profile is generated relative to the max in _get_utility_rate_parameters
199199
ret['system_capacity'] = _get_max_net_generation_MW(model) * 1e3
200200

201-
# TODO utilization factor = nominal capacity factor
202-
203201
geophires_ctr_tenths = Decimal(econ.CTR.value)
204202
fed_ratio = 0.75
205203
fed_rate_tenths = geophires_ctr_tenths * (Decimal(fed_ratio))
@@ -217,7 +215,7 @@ def pct(econ_value: Parameter) -> float:
217215
ret['ptc_fed_term'] = econ.PTCDuration.quantity().to(convertible_unit('yr')).magnitude
218216

219217
if econ.PTCInflationAdjusted.value:
220-
ret['ptc_fed_escal'] = pct(econ.RINFL)
218+
ret['ptc_fed_escal'] = _pct(econ.RINFL)
221219

222220
# 'Property Tax Rate'
223221
geophires_ptr_tenths = Decimal(econ.PTR.value)
@@ -232,24 +230,26 @@ def pct(econ_value: Parameter) -> float:
232230
)
233231

234232
# Debt/equity ratio ('Fraction of Investment in Bonds' parameter)
235-
ret['debt_percent'] = pct(econ.FIB)
233+
ret['debt_percent'] = _pct(econ.FIB)
236234

237235
# Interest rate
238-
ret['real_discount_rate'] = pct(econ.discountrate)
236+
ret['real_discount_rate'] = _pct(econ.discountrate)
239237

240238
# Project lifetime
241239
ret['term_tenor'] = model.surfaceplant.plant_lifetime.value
242-
ret['term_int_rate'] = pct(econ.BIR)
240+
ret['term_int_rate'] = _pct(econ.BIR)
243241

244242
# TODO 'Inflated Equity Interest Rate' (may not have equivalent in SAM...?)
245243

246244
ret['ibi_oth_amount'] = (econ.OtherIncentives.quantity() + econ.TotalGrant.quantity()).to('USD').magnitude
247245

248-
ret['user_capacity_factor'] = model.surfaceplant.utilization_factor.value
249-
250246
return ret
251247

252248

249+
def _pct(econ_value: Parameter) -> float:
250+
return econ_value.quantity().to(convertible_unit('%')).magnitude
251+
252+
253253
def _ppa_pricing_model(
254254
plant_lifetime: int, start_price: float, end_price: float, escalation_start_year: int, escalation_rate: float
255255
) -> list:

tests/base_test_case.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import os.path
44
import unittest
55

6+
from geophires_x_client import _get_logger
7+
68

79
class BaseTestCase(unittest.TestCase):
810
maxDiff = None
@@ -17,10 +19,19 @@ def _get_test_file_content(self, test_file_name):
1719
def _list_test_files_dir(self, test_files_dir: str):
1820
return os.listdir(self._get_test_file_path(test_files_dir)) # noqa: PTH208
1921

20-
def assertAlmostEqualWithinPercentage(self, expected, actual, msg=None, percent=5):
22+
def assertAlmostEqualWithinPercentage(self, expected, actual, msg: str | None = None, percent=5):
23+
if msg is not None and not isinstance(msg, str):
24+
raise ValueError(f'msg must be a string (you may have meant to pass percent={msg})')
25+
2126
if isinstance(expected, numbers.Real):
2227
self.assertAlmostEqual(expected, actual, msg=msg, delta=abs(percent / 100.0 * expected))
2328
else:
29+
if isinstance(expected, list) and isinstance(actual, list):
30+
suggest = f'self.assertListAlmostEqual({expected}, {actual}, msg={msg}, percent={percent})'
31+
suggest = f'Got 2 lists, you probably meant to call:\n\t{suggest}'
32+
log = _get_logger(__name__)
33+
log.warning(suggest)
34+
2435
self.assertEqual(expected, actual, msg)
2536

2637
def assertDictAlmostEqual(self, expected, actual, msg=None, places=7, percent=None):

tests/geophires_x_tests/test_economics_sam.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -116,13 +116,13 @@ def get_row(name: str) -> list[float]:
116116

117117
sam_gen_profile = get_row('Electricity to grid net (kWh)')
118118

119-
# Discrepancy is probably due to windowing and/or rounding effects
120-
# (TODO to investigate further when time permits)
121-
allowed_delta_percent = 15
119+
# Discrepancy is probably due to windowing and/or rounding effects,
120+
# may merit further investigation when time permits.
121+
allowed_delta_percent = 5
122122
self.assertAlmostEqualWithinPercentage(
123123
geophires_avg_net_gen_GWh,
124124
np.average(sam_gen_profile) * 1e-6,
125-
allowed_delta_percent,
125+
percent=allowed_delta_percent,
126126
)
127127

128128
elec_idx = r.result['HEAT AND/OR ELECTRICITY EXTRACTION AND GENERATION PROFILE'][0].index(
@@ -343,6 +343,22 @@ def get_row(name: str):
343343
{'Production Tax Credit Electricity': 0.04, 'Production Tax Credit Duration': 9}, shortened_term_expected
344344
)
345345

346+
def test_capacity_factor(self):
347+
r_90 = self._get_result({'Utilization Factor': 0.9})
348+
cash_flow_90 = r_90.result['SAM CASH FLOW PROFILE']
349+
350+
r_20 = self._get_result({'Utilization Factor': 0.2})
351+
cash_flow_20 = r_20.result['SAM CASH FLOW PROFILE']
352+
353+
etg = 'Electricity to grid (kWh)'
354+
355+
etg_90 = EconomicsSamTestCase._get_cash_flow_row(cash_flow_90, etg)
356+
etg_20 = EconomicsSamTestCase._get_cash_flow_row(cash_flow_20, etg)
357+
358+
etg_20_expected = [(etg_90_entry / 0.9) * 0.2 for etg_90_entry in etg_90]
359+
360+
self.assertListAlmostEqual(etg_20_expected, etg_20, percent=1)
361+
346362
def test_clean_profile(self):
347363
profile = [
348364
['foo', 1, 2, 3],

tests/test_base_test_case.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,25 @@ def test_assertAlmostEqualWithinPercentage(self):
2525
with self.assertRaises(AssertionError):
2626
self.assertListAlmostEqual([1, 2, 3], [1.1, 2.2, 3.3], percent=5)
2727

28+
def test_assertAlmostEqualWithinPercentage_bad_arguments(self):
29+
with self.assertRaises(ValueError) as msg_type_error:
30+
self.assertAlmostEqualWithinPercentage(100, 100, 10)
31+
32+
self.assertIn(str(msg_type_error), '(you may have meant to pass percent=10)')
33+
34+
def assertHasLogRecordWithMessage(logs_, message):
35+
assert message in [record.message for record in logs_.records]
36+
37+
with self.assertLogs(level='INFO') as logs:
38+
with self.assertRaises(AssertionError):
39+
self.assertAlmostEqualWithinPercentage([1, 2, 3], [1.1, 2.2, 3.3], percent=10.5)
40+
41+
assertHasLogRecordWithMessage(
42+
logs,
43+
'Got 2 lists, you probably meant to call:\n\t'
44+
'self.assertListAlmostEqual([1, 2, 3], [1.1, 2.2, 3.3], msg=None, percent=10.5)',
45+
)
46+
2847

2948
if __name__ == '__main__':
3049
unittest.main()

0 commit comments

Comments
 (0)