Skip to content

Commit 154d534

Browse files
_insert_calculated_levelized_metrics_line_items code cleanup
1 parent a06d8b6 commit 154d534

1 file changed

Lines changed: 49 additions & 116 deletions

File tree

src/geophires_x/EconomicsSamCalculations.py

Lines changed: 49 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,6 @@ def _for_operational_years(_row: list[Any]) -> list[Any]:
246246

247247
return ret
248248

249-
# noinspection DuplicatedCode
250249
def _insert_calculated_levelized_metrics_line_items(self, cf_ret: list[list[Any]]) -> list[list[Any]]:
251250
ret = cf_ret.copy()
252251

@@ -286,9 +285,21 @@ def _insert_row_before(before_row_name: str, row_name: str, row_content: list[An
286285
def _insert_blank_line_before(before_row_name: str) -> None:
287286
_insert_row_before(before_row_name, '', ['' for _it in ret[_get_row_index(before_row_name)]][1:])
288287

288+
def _calculate_pv_year_0(cash_flow_array: list) -> int:
289+
"""Calculate the absolute present value at Year 0 for a cash flow array using npf.npv."""
290+
return abs(
291+
round(
292+
npf.npv(
293+
self.nominal_discount_rate.quantity().to('dimensionless').magnitude,
294+
cash_flow_array,
295+
)
296+
)
297+
)
298+
289299
after_tax_lcoe_and_ppa_price_header_row_title = 'AFTER-TAX LCOE AND PPA PRICE'
290300

291-
# Backfill annual costs
301+
# --- Backfill annual costs ---
302+
# Pre-revenue years use after-tax net cash flow; operational years use SAM's annual costs.
292303
annual_costs_usd_row_name = 'Annual costs ($)'
293304
annual_costs = cf_ret[_get_row_index(annual_costs_usd_row_name)].copy()
294305
after_tax_net_cash_flow_usd = cf_ret[_get_row_index('After-tax net cash flow ($)')]
@@ -324,42 +335,19 @@ def _insert_blank_line_before(before_row_name: str) -> None:
324335
)
325336
ret[electricity_to_grid_kwh_row_index][1:] = electricity_to_grid_backfilled
326337

327-
pv_of_annual_costs_backfilled_row_name = 'Present value of annual costs ($)'
328-
329-
# Backfill PV of annual costs
330-
annual_costs_backfilled_pv_processed = annual_costs_backfilled.copy()
331-
pv_of_annual_costs_backfilled = []
332-
for year in range(self._pre_revenue_years_count):
333-
pv_at_year = abs(
334-
round(
335-
npf.npv(
336-
self.nominal_discount_rate.quantity().to('dimensionless').magnitude,
337-
annual_costs_backfilled_pv_processed,
338-
)
339-
)
340-
)
341-
342-
pv_of_annual_costs_backfilled.append(pv_at_year)
343-
344-
cost_at_year = annual_costs_backfilled_pv_processed.pop(0)
345-
annual_costs_backfilled_pv_processed[0] = annual_costs_backfilled_pv_processed[0] + cost_at_year
346-
347-
pv_of_annual_costs_backfilled_row = [
348-
*[pv_of_annual_costs_backfilled_row_name],
349-
*pv_of_annual_costs_backfilled,
350-
]
338+
# --- PV of annual costs at Year 0 ---
339+
pv_costs_year_0 = _calculate_pv_year_0(annual_costs_backfilled)
351340

352341
pv_of_annual_costs_row_name = 'Present value of annual costs ($)'
353342
pv_of_annual_costs_row_index = _get_row_index(pv_of_annual_costs_row_name)
354343
ret[pv_of_annual_costs_row_index][1:] = [
355-
pv_of_annual_costs_backfilled[0],
344+
pv_costs_year_0,
356345
*([''] * (self._pre_revenue_years_count - 1)),
357346
]
358347

348+
# --- PV of annual energy costs (electrical portion) ---
359349
pv_of_annual_energy_costs_row_name = 'Present value of annual energy costs ($)'
360-
pv_of_annual_energy_costs_at_year_0_usd = int(
361-
round(pv_of_annual_costs_backfilled[0] * self.electricity_plant_frac_of_capex)
362-
)
350+
pv_of_annual_energy_costs_at_year_0_usd = int(round(pv_costs_year_0 * self.electricity_plant_frac_of_capex))
363351
_insert_row_before(
364352
'Present value of annual energy nominal (kWh)',
365353
pv_of_annual_energy_costs_row_name,
@@ -369,66 +357,38 @@ def _insert_blank_line_before(before_row_name: str) -> None:
369357
)
370358
_insert_blank_line_before(pv_of_annual_energy_costs_row_name)
371359

372-
# Backfill PV of electricity to grid
373-
electricity_to_grid_backfilled_pv_processed = electricity_to_grid_backfilled.copy()
374-
pv_of_electricity_to_grid_backfilled_kwh = []
375-
for year in range(self._pre_revenue_years_count):
376-
pv_at_year = abs(
377-
round(
378-
npf.npv(
379-
self.nominal_discount_rate.quantity().to('dimensionless').magnitude,
380-
electricity_to_grid_backfilled_pv_processed,
381-
)
382-
)
383-
)
384-
385-
pv_of_electricity_to_grid_backfilled_kwh.append(pv_at_year)
386-
387-
electricity_to_grid_at_year = electricity_to_grid_backfilled_pv_processed.pop(0)
388-
electricity_to_grid_backfilled_pv_processed[0] = (
389-
electricity_to_grid_backfilled_pv_processed[0] + electricity_to_grid_at_year
390-
)
360+
# --- PV of electricity to grid at Year 0 ---
361+
pv_electricity_to_grid_year_0_kwh = _calculate_pv_year_0(electricity_to_grid_backfilled)
391362

392363
pv_of_annual_energy_row_name = 'Present value of annual energy nominal (kWh)'
393364
for pv_of_annual_energy_row_index in _get_row_indexes(pv_of_annual_energy_row_name):
394365
ret[pv_of_annual_energy_row_index][1:] = [
395-
pv_of_electricity_to_grid_backfilled_kwh[0],
366+
pv_electricity_to_grid_year_0_kwh,
396367
*([''] * (self._pre_revenue_years_count - 1)),
397368
]
398369

370+
# --- LCOE nominal ---
399371
def backfill_lcoe_nominal() -> None:
400-
pv_of_electricity_to_grid_backfilled_row_kwh = pv_of_electricity_to_grid_backfilled_kwh
401-
pv_of_annual_energy_costs_usd = [
402-
it * self.electricity_plant_frac_of_capex
403-
for it in pv_of_annual_costs_backfilled_row[
404-
1 if isinstance(pv_of_annual_costs_backfilled_row[0], str) else 0 :
405-
]
406-
]
372+
pv_energy_costs_year_0_usd = pv_costs_year_0 * self.electricity_plant_frac_of_capex
407373

408-
lcoe_nominal_backfilled = []
409-
for _year in range(len(pv_of_annual_energy_costs_usd)):
410-
entry: float | str = 'NaN'
411-
if pv_of_electricity_to_grid_backfilled_row_kwh[_year] != 0:
412-
entry = (
413-
pv_of_annual_energy_costs_usd[_year] * 100 / pv_of_electricity_to_grid_backfilled_row_kwh[_year]
414-
)
415-
416-
lcoe_nominal_backfilled.append(entry)
374+
lcoe_nominal_entry: float | str = 'NaN'
375+
if pv_electricity_to_grid_year_0_kwh != 0:
376+
lcoe_nominal_entry = pv_energy_costs_year_0_usd * 100 / pv_electricity_to_grid_year_0_kwh
417377

418378
lcoe_nominal_row_name = 'LCOE Levelized cost of energy nominal (cents/kWh)'
419379
lcoe_nominal_row_index = _get_row_index(lcoe_nominal_row_name)
420380

421-
lcoe_nominal_backfilled_entry = lcoe_nominal_backfilled[0]
422-
if isinstance(lcoe_nominal_backfilled_entry, float):
423-
lcoe_nominal_backfilled_entry = round(lcoe_nominal_backfilled_entry, 2)
381+
if isinstance(lcoe_nominal_entry, float):
382+
lcoe_nominal_entry = round(lcoe_nominal_entry, 2)
424383

425384
ret[lcoe_nominal_row_index][1:] = [
426-
lcoe_nominal_backfilled_entry,
385+
lcoe_nominal_entry,
427386
*([None] * (self._pre_revenue_years_count - 1)),
428387
]
429388

430389
backfill_lcoe_nominal()
431390

391+
# --- LPPA metrics ---
432392
def backfill_lppa_metrics() -> None:
433393
pv_of_ppa_revenue_row_index = _get_row_index_after(
434394
'Present value of PPA revenue ($)', after_tax_lcoe_and_ppa_price_header_row_title
@@ -476,6 +436,7 @@ def backfill_lppa_metrics() -> None:
476436

477437
backfill_lppa_metrics()
478438

439+
# --- Non-electricity levelized metrics (LCOH, LCOC) ---
479440
def insert_non_electricity_levelized_metrics(
480441
amount_provided_kwh_row_name: str, # = 'Heat provided (kWh)',
481442
amount_provided_unit: str, # = 'MMBTU',
@@ -505,48 +466,21 @@ def insert_non_electricity_levelized_metrics(
505466

506467
ret[amount_provided_kwh_row_index][1:] = amount_provided_backfilled
507468

508-
# <Back>fill PV of heat provided
509-
amount_provided_backfilled_pv_processed = amount_provided_backfilled.copy()
510-
pv_of_amount_provided_backfilled_kwh = []
511-
for year_ in range(self._pre_revenue_years_count):
512-
pv_at_year_ = abs(
513-
round(
514-
npf.npv(
515-
self.nominal_discount_rate.quantity().to('dimensionless').magnitude,
516-
amount_provided_backfilled_pv_processed,
517-
)
518-
)
519-
)
469+
# PV of amount provided (e.g. heat) at Year 0
470+
pv_amount_provided_year_0_kwh = _calculate_pv_year_0(amount_provided_backfilled)
520471

521-
pv_of_amount_provided_backfilled_kwh.append(pv_at_year_)
472+
# Thermal portion of PV of annual costs at Year 0
473+
pv_non_elec_costs_year_0_usd = pv_costs_year_0 * (1.0 - self.electricity_plant_frac_of_capex)
522474

523-
amount_provided_at_year = amount_provided_backfilled_pv_processed.pop(0)
524-
amount_provided_backfilled_pv_processed[0] = (
525-
amount_provided_backfilled_pv_processed[0] + amount_provided_at_year
475+
# Levelized cost = thermal costs / amount provided (converted to target unit)
476+
levelized_cost_nominal_entry: float | str = 'NaN'
477+
if pv_amount_provided_year_0_kwh != 0:
478+
levelized_cost_nominal_entry = (
479+
pv_non_elec_costs_year_0_usd
480+
/ quantity(pv_amount_provided_year_0_kwh, 'kWh').to(amount_provided_unit).magnitude
526481
)
527482

528-
pv_of_amount_provided_backfilled_row_kwh = pv_of_amount_provided_backfilled_kwh
529-
pv_of_annual_non_elec_type_costs_backfilled_row_values_usd = [
530-
it * (1.0 - self.electricity_plant_frac_of_capex)
531-
for it in pv_of_annual_costs_backfilled_row[
532-
1 if isinstance(pv_of_annual_costs_backfilled_row[0], str) else 0 :
533-
]
534-
]
535-
536-
lcoh_nominal_backfilled = []
537-
for _year in range(len(pv_of_annual_non_elec_type_costs_backfilled_row_values_usd)):
538-
entry: float | str = 'NaN'
539-
if pv_of_amount_provided_backfilled_row_kwh[_year] != 0:
540-
entry = (
541-
pv_of_annual_non_elec_type_costs_backfilled_row_values_usd[_year]
542-
/ quantity(pv_of_amount_provided_backfilled_row_kwh[_year], 'kWh')
543-
.to(amount_provided_unit)
544-
.magnitude
545-
)
546-
547-
lcoh_nominal_backfilled.append(entry)
548-
549-
# Insert new row if LCOE row does not exist (yet)
483+
# Insert new row if levelized cost row does not exist (yet)
550484
levelized_cost_nominal_row_index = _get_row_index(
551485
levelized_cost_nominal_row_name, raise_exception_if_not_present=False
552486
)
@@ -556,16 +490,15 @@ def insert_non_electricity_levelized_metrics(
556490
_insert_blank_line_before('PROJECT STATE INCOME TAXES')
557491
levelized_cost_nominal_row_index = _get_row_index(levelized_cost_nominal_row_name)
558492

559-
levelized_cost_nominal_backfilled_entry = lcoh_nominal_backfilled[0]
560-
if isinstance(levelized_cost_nominal_backfilled_entry, float):
561-
levelized_cost_nominal_backfilled_entry = round(levelized_cost_nominal_backfilled_entry, 2)
493+
if isinstance(levelized_cost_nominal_entry, float):
494+
levelized_cost_nominal_entry = round(levelized_cost_nominal_entry, 2)
562495

563496
ret[levelized_cost_nominal_row_index][1:] = [
564-
levelized_cost_nominal_backfilled_entry,
497+
levelized_cost_nominal_entry,
565498
*([None] * (self._pre_revenue_years_count - 1)),
566499
]
567500

568-
# Insert new row if PV of heat costs row does not exist (yet)
501+
# Insert new row if PV of non-electricity costs row does not exist (yet)
569502
pv_annual_non_elec_type_costs_row_index = _get_row_index(
570503
pv_annual_non_elec_type_costs_row_name, raise_exception_if_not_present=False
571504
)
@@ -574,7 +507,7 @@ def insert_non_electricity_levelized_metrics(
574507
_insert_row_before(levelized_cost_nominal_row_name, pv_annual_non_elec_type_costs_row_name, None)
575508
pv_annual_non_elec_type_costs_row_index = _get_row_index(pv_annual_non_elec_type_costs_row_name)
576509

577-
pv_annual_non_elec_type_costs_entry = pv_of_annual_non_elec_type_costs_backfilled_row_values_usd[0]
510+
pv_annual_non_elec_type_costs_entry = pv_non_elec_costs_year_0_usd
578511
if isinstance(pv_annual_non_elec_type_costs_entry, float):
579512
pv_annual_non_elec_type_costs_entry = int(round(pv_annual_non_elec_type_costs_entry, 2))
580513

@@ -587,7 +520,7 @@ def insert_non_electricity_levelized_metrics(
587520
pv_of_annual_amount_provided_row_name = (
588521
f'{pv_of_annual_amount_provided_row_base_name} ({pv_of_annual_amount_provided_unit})'
589522
)
590-
# Insert new row if PV of heat provided row does not exist (yet)
523+
# Insert new row if PV of amount provided row does not exist (yet)
591524
pv_of_annual_amount_provided_row_index = _get_row_index(
592525
pv_of_annual_amount_provided_row_name, raise_exception_if_not_present=False
593526
)
@@ -596,7 +529,7 @@ def insert_non_electricity_levelized_metrics(
596529
_insert_row_before(levelized_cost_nominal_row_name, pv_of_annual_amount_provided_row_name, None)
597530
pv_of_annual_amount_provided_row_index = _get_row_index(pv_of_annual_amount_provided_row_name)
598531

599-
pv_annual_amount_provided_entry = pv_of_amount_provided_backfilled_row_kwh[0]
532+
pv_annual_amount_provided_entry = pv_amount_provided_year_0_kwh
600533
if any(isinstance(pv_annual_amount_provided_entry, it) for it in [int, float]):
601534
pv_annual_amount_provided_entry = int(
602535
round(

0 commit comments

Comments
 (0)