Skip to content

Commit 8110eeb

Browse files
display Other Incentives and Total Grants as Total CAPEX modifiers (not OCC modifiers). update SAM-EM docs.
1 parent ce728ef commit 8110eeb

9 files changed

Lines changed: 67 additions & 24 deletions

docs/SAM-Economic-Models.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ The following table describes how GEOPHIRES parameters are transformed into SAM
2020
| `Maximum Total Electricity Generation` | Generation Profile | `Nameplate capacity` | `Singleowner` | `system_capacity` | .. N/A |
2121
| `Utilization Factor` | Generation Profile | `Nominal capacity factor` | `Singleowner` | `user_capacity_factor` | .. N/A |
2222
| `Net Electricity Generation` | AC Degradation | `Annual AC degradation rate` schedule | `Utilityrate5` | `degradation` | Percentage difference of each year's `Net Electricity Generation` from `Maximum Total Electricity Generation` is input as SAM as the degradation rate schedule in order to match SAM's generation profile to GEOPHIRES |
23-
| `Total CAPEX` | Installation Costs | `Total Installed Cost` | `Singleowner` | `total_installed_cost` | `Total CAPEX` = `Overnight Capital Cost` + `Inflation costs during construction` + `Interest during construction` |
23+
| `Total CAPEX` | Installation Costs | `Total Installed Cost` | `Singleowner` | `total_installed_cost` | `Total CAPEX` = `Overnight Capital Cost` + `Inflation costs during construction` + `Interest during construction` - `Other Incentives` - `One-time Grants Etc` |
2424
| `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 |
2525
| `Royalty Supplemental Payments` | Operating Costs | `Fixed operating cost` schedule | `Singleowner` | `om_fixed` | Royalty supplemental payments during the operational phase are added to the fixed operating cost according to the schedule. |
2626
| `Royalty Rate`, `Royalty Rate Escalation`, `Royalty Rate Escalation Start Year`, `Royalty Rate Maximum`; `Royalty Rate Schedule` | Operating Costs | `Variable operating cost` | `Singleowner` | `om_production` | The royalty is modeled as a tax-deductible variable operating expense. GEOPHIRES uses the provided schedule, or calculates a schedule of $/MWh values based on the PPA price and Royalty Rate for each year, with optional escalation, escalation start year, and cap (maximum). This ensures the total annual expense in SAM accurately matches the royalty payment due on gross revenue. |
0 Bytes
Loading
0 Bytes
Loading
0 Bytes
Loading
-3.71 KB
Loading

src/geophires_x/EconomicsSam.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -238,12 +238,13 @@ def sf(_v: float, num_sig_figs: int = 5) -> float:
238238
sam_economics.after_tax_irr.value = sf(_get_after_tax_irr_pct(single_owner, cash_flow_operational_years, model))
239239

240240
sam_economics.project_npv.value = sf(_get_project_npv_musd(single_owner, cash_flow_operational_years, model))
241+
241242
# Add back ibi_oth_amount (OtherIncentives + TotalGrant) which SAM subtracts from
242243
# total_installed_cost to compute adjusted_installed_cost. Incentives are still applied
243-
# by SAM natively in the cash flow / tax basis calculations; we just don't want them to
244+
# by SAM natively in the cash flow/tax basis calculations; we just don't want them to
244245
# also reduce the reported Total CAPEX (which would be inconsistent with Overnight Capital
245-
# Cost, since CCap is no longer reduced by incentives for SAM Economic Models -- see
246-
# Economics.calculate_capital_costs).
246+
# Cost, since CCap is not reduced by incentives for SAM Economic Models - see
247+
# Economics.calculate_total_capital_costs).
247248
_ibi_oth_usd = (
248249
(model.economics.OtherIncentives.quantity() + model.economics.TotalGrant.quantity()).to('USD').magnitude
249250
)

src/geophires_x/Outputs.py

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -529,21 +529,28 @@ def PrintOutputs(self, model: Model):
529529
# Note ITC is in ECONOMIC PARAMETERS category for SAM-EM (not capital costs)
530530
f.write(f' {econ.RITCValue.display_name}: {-1 * econ.RITCValue.value:10.2f} {econ.RITCValue.CurrentUnits.value}\n')
531531

532-
additional_capex_modifiers: list[tuple[Parameter, int]] = [
533-
(econ.FlatLicenseEtc, 1),
534-
(econ.OtherIncentives, -1), # FIXME WIP display after OCC instead of before
535-
(econ.TotalGrant, -1)
536-
]
537-
for additional_capex_modifier_entry in additional_capex_modifiers:
538-
additional_capex_modifier_param: Parameter = additional_capex_modifier_entry[0]
539-
additional_capex_modifier_multiplier: int = additional_capex_modifier_entry[1]
532+
def _render_additional_capital_cost_modifiers(additional_modifiers: list[tuple[Parameter, int]]) -> None:
533+
for additional_modifier_entry in additional_modifiers:
534+
additional_modifier_param: Parameter = additional_modifier_entry[0]
535+
additional_modifier_multiplier: int = additional_modifier_entry[1]
540536

541-
acm_render_value = additional_capex_modifier_param.value * additional_capex_modifier_multiplier
537+
am_render_value = additional_modifier_param.value * additional_modifier_multiplier
542538

543-
if additional_capex_modifier_param.Provided:
544-
acm_label = Outputs._field_label(additional_capex_modifier_param.Name, 47)
545-
f.write(
546-
f' {acm_label}{acm_render_value:10.2f} {additional_capex_modifier_param.CurrentUnits.value}\n')
539+
if additional_modifier_param.Provided:
540+
am_label = Outputs._field_label(additional_modifier_param.Name, 47)
541+
f.write(
542+
f' {am_label}{am_render_value:10.2f} {additional_modifier_param.CurrentUnits.value}\n')
543+
544+
additional_occ_modifiers: list[tuple[Parameter, int]] = [
545+
(econ.FlatLicenseEtc, 1),
546+
]
547+
if not is_sam_econ_model:
548+
# For SAM-EM these modify Total CAPEX, not OCC
549+
additional_occ_modifiers.extend([
550+
(econ.OtherIncentives, -1),
551+
(econ.TotalGrant, -1)
552+
])
553+
_render_additional_capital_cost_modifiers(additional_occ_modifiers)
547554

548555
if is_sam_econ_model and econ.DoAddOnCalculations.value:
549556
# Non-SAM econ models print this in Extended Economics profile
@@ -571,6 +578,13 @@ def PrintOutputs(self, model: Model):
571578
f.write(
572579
f' {idc_label}{econ.interest_during_construction.value:10.2f} {econ.interest_during_construction.CurrentUnits.value}\n')
573580

581+
additional_total_capex_modifiers: list[tuple[Parameter, int]] = [
582+
(econ.OtherIncentives, -1),
583+
(econ.TotalGrant, -1)
584+
] if is_sam_econ_model else []
585+
586+
_render_additional_capital_cost_modifiers(additional_total_capex_modifiers)
587+
574588
capex_param = econ.CCap if not is_sam_econ_model else econ.capex_total
575589
capex_label = Outputs._field_label(capex_param.display_name, 50)
576590
f.write(f' {capex_label}{capex_param.value:10.2f} {capex_param.CurrentUnits.value}\n')

src/geophires_x_client/geophires_x_result.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -275,14 +275,14 @@ class GeophiresXResult:
275275
'Total surface equipment costs',
276276
'Investment Tax Credit',
277277
'One-time Flat License Fees Etc',
278-
'Other Incentives',
279-
'One-time Grants Etc',
280278
'Total Add-on CAPEX',
281279
'Overnight Capital Cost',
282280
# Displayed for economic models that treat inflation costs as capital costs (SAM-EM)
283281
'Inflation costs during construction',
284282
'Royalty supplemental payments during construction',
285283
'Interest during construction',
284+
'Other Incentives',
285+
'One-time Grants Etc',
286286
'Total capital costs',
287287
'Annualized capital costs',
288288
# AGS/CLGS

tests/geophires_x_tests/test_economics_sam.py

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1435,12 +1435,40 @@ def test_other_incentives_and_one_time_flat_fees(self):
14351435
)
14361436
)
14371437

1438-
self.assertGreater(
1439-
r.result['CAPITAL COSTS (M$)']['Total CAPEX']['value'],
1440-
r.result['CAPITAL COSTS (M$)']['Overnight Capital Cost']['value'],
1441-
)
1438+
with open(r.output_file_path, encoding='utf-8') as f:
1439+
lines = f.readlines()
1440+
1441+
is_parsing = False
1442+
occ_val = 0.0
1443+
total_capex_val = 0.0
1444+
intermediate_sum = 0.0
1445+
1446+
for line in lines:
1447+
clean_line = line.strip()
1448+
1449+
if clean_line.startswith('Overnight Capital Cost:'):
1450+
is_parsing = True
1451+
occ_val = float(clean_line.split(':')[1].replace('MUSD', '').strip())
1452+
continue
1453+
1454+
if is_parsing:
1455+
if clean_line.startswith('Total CAPEX:'):
1456+
total_capex_val = float(clean_line.split(':')[1].replace('MUSD', '').strip())
1457+
break
14421458

1443-
# FIXME WIP verify OCC -> Total CAPEX sum, accounting for incentives and grants
1459+
if ':' in clean_line:
1460+
val_str = clean_line.split(':')[1].replace('MUSD', '').strip()
1461+
try:
1462+
intermediate_sum += float(val_str)
1463+
except ValueError:
1464+
pass
1465+
1466+
self.assertAlmostEqual(
1467+
occ_val + intermediate_sum,
1468+
total_capex_val,
1469+
places=2,
1470+
msg='Total CAPEX should be the sum of OCC and all intermediate items (e.g., inflation, interest, incentives)',
1471+
)
14441472

14451473
@staticmethod
14461474
def _new_model(

0 commit comments

Comments
 (0)