Skip to content

Commit 4de04d4

Browse files
Merge branch 'capacity-factor' into econ-model-single-owner-ppa-sam-2
2 parents 74dd284 + 3441053 commit 4de04d4

File tree

7 files changed

+69
-17
lines changed

7 files changed

+69
-17
lines changed

docs/SAM-Economic-Models.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ The following table describes how GEOPHIRES parameters are transformed into SAM
2626
| GEOPHIRES Parameter(s) | SAM Category | SAM Input(s) | SAM Module(s) | SAM Parameter Name(s) | Comment |
2727
|-------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------|----------------------------------------------------------------------------------|-----------------------------------|--------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
2828
| `Max Net Electricity Production` | Generation Profile | `Nameplate capacity` | `Singleowner` | `system_capacity` | .. N/A |
29+
| `Utilizaton Factor` | Generation Profile | `Nominal capacity factor` | `Singleowner` | `user_capacity_factor` | .. N/A |
2930
| `Net Electricity Production` | AC Degradation | `Annual AC degradation rate` schedule | `Utilityrate5` | `degradation` | Percentage difference of each year's `Net Electricity Production` from `Max Net Electricity Production` is input as SAM as the degradation rate schedule in order to match SAM's generation profile to GEOPHIRES |
3031
| `Total Capital Cost` + `Investment Tax Credit Value` | Installation Costs | `Total Installed Cost` | `Singleowner` | `total_installed_cost` | .. N/A |
3132
| `Total O&M Cost`, `Inflation Rate` | Operating Costs | `Fixed operating cost`, `Escalation rate` set to `Inflation Rate` × -1 | `Singleowner` | `om_fixed`, `om_fixed_escal` | .. N/A |

src/geophires_x/EconomicsSam.py

Lines changed: 13 additions & 11 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,14 +230,14 @@ 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

@@ -248,6 +246,10 @@ def pct(econ_value: Parameter) -> float:
248246
return ret
249247

250248

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

src/geophires_x/Outputs.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,9 @@ def PrintOutputs(self, model: Model):
257257
irfl = Outputs._field_label(econ.interest_rate.Name, 49)
258258
f.write(f' {irfl}{econ.interest_rate.value:10.2f} {econ.interest_rate.CurrentUnits.value}\n')
259259

260+
# FIXME TODO unit is missing
260261
f.write(f' Accrued financing during construction: {model.economics.inflrateconstruction.value*100:10.2f} {model.economics.inflrateconstruction.CurrentUnits.value}\n')
262+
261263
f.write(f' Project lifetime: {model.surfaceplant.plant_lifetime.value:10.0f} {model.surfaceplant.plant_lifetime.CurrentUnits.value}\n')
262264
f.write(f' Capacity factor: {model.surfaceplant.utilization_factor.value * 100:10.1f} %\n')
263265

src/geophires_x/SurfacePlant.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -305,7 +305,8 @@ def __init__(self, model: Model):
305305
CurrentUnits=PercentUnit.TENTH,
306306
Required=True,
307307
ErrMessage="assume default utilization factor (0.9)",
308-
ToolTipText="Ratio of the time the plant is running in normal production in a 1-year time period."
308+
ToolTipText="Ratio of the time the plant is running in normal production in a 1-year time period. "
309+
"Synonymous with capacity factor."
309310
)
310311
self.enduse_efficiency_factor = self.ParameterDict[self.enduse_efficiency_factor.Name] = floatParameter(
311312
"End-Use Efficiency Factor",

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)