@@ -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