@@ -678,15 +678,59 @@ def _get_single_owner_parameters(model: Model) -> dict[str, Any]:
678678 ret ['federal_tax_rate' ], ret ['state_tax_rate' ] = _get_fed_and_state_tax_rates (econ .CTR .value )
679679
680680 geophires_itc_tenths = Decimal (econ .RITC .value )
681- ret ['itc_fed_percent' ] = [float (geophires_itc_tenths * Decimal (100 ))]
682681
683- if econ .PTCElec .Provided :
684- ret ['ptc_fed_amount' ] = [econ .PTCElec .quantity ().to (convertible_unit ('USD/kWh' )).magnitude ]
682+ if econ .DoSDACGTCalculations .value and geophires_itc_tenths > 0 :
683+ # Shield DAC CAPEX from ITC to prevent unlawful MACRS basis reduction
684+ max_carbon_capacity_tonnes = max (model .sdacgteconomics .CarbonExtractedAnnually .value )
685+ sdac_capex_usd = (
686+ model .sdacgteconomics .CAPEX .value * model .sdacgteconomics .CAPEX_mult .value * max_carbon_capacity_tonnes
687+ )
688+
689+ # Geothermal eligible basis = Total Installed Cost - DAC CAPEX
690+ eligible_geothermal_basis_usd = ret ['total_installed_cost' ] - sdac_capex_usd
691+ itc_fed_amount_usd = float (Decimal (eligible_geothermal_basis_usd ) * geophires_itc_tenths )
685692
686- ret ['ptc_fed_term' ] = econ .PTCDuration .quantity ().to (convertible_unit ('yr' )).magnitude
693+ ret ['itc_fed_percent' ] = [0.0 ] # Disable percentage-based ITC on the whole project
694+ ret ['itc_fed_amount' ] = [itc_fed_amount_usd ] # Inject fixed ITC amount for geothermal only
695+ else :
696+ ret ['itc_fed_percent' ] = [float (geophires_itc_tenths * Decimal (100 ))]
697+
698+ # Build a year-by-year schedule for the Federal Production Tax Credit
699+ ptc_fed_amount_schedule = [0.0 ] * model .surfaceplant .plant_lifetime .value
687700
688- if econ .PTCInflationAdjusted .value :
689- ret ['ptc_fed_escal' ] = _pct (econ .RINFL )
701+ # 1. Base Electricity PTC
702+ if econ .PTCElec .Provided :
703+ base_ptc_rate = econ .PTCElec .quantity ().to (convertible_unit ('USD/kWh' )).magnitude
704+ ptc_term = int (econ .PTCDuration .quantity ().to (convertible_unit ('yr' )).magnitude )
705+ for i in range (min (ptc_term , model .surfaceplant .plant_lifetime .value )):
706+ escalation = (1.0 + _pct (econ .RINFL ) / 100.0 ) ** i if econ .PTCInflationAdjusted .value else 1.0
707+ ptc_fed_amount_schedule [i ] = base_ptc_rate * escalation
708+
709+ # 2. S-DAC 45Q Equivalent (Mapped as Non-Taxable PBI to avoid MACRS/Exclusivity issues)
710+ if econ .DoSDACGTCalculations .value :
711+ pbi_oth_amount_schedule = [0.0 ] * model .surfaceplant .plant_lifetime .value
712+
713+ for i in range (model .surfaceplant .plant_lifetime .value ):
714+ # The statutory duration cutoff (e.g., 12 years) is already handled inside EconomicsS_DAC_GT.py
715+ # so CarbonRevenue will naturally be 0.0 for years > 12.
716+ sdac_revenue_usd = model .sdacgteconomics .CarbonRevenue .value [i ]
717+ net_kwh_produced = model .surfaceplant .NetkWhProduced .value [i ]
718+
719+ # Convert absolute S-DAC revenue (USD) into an equivalent USD/kWh PBI rate for SAM
720+ if net_kwh_produced > 0 :
721+ pbi_oth_amount_schedule [i ] = sdac_revenue_usd / net_kwh_produced
722+
723+ if any (rate > 0.0 for rate in pbi_oth_amount_schedule ):
724+ ret ['pbi_oth_amount' ] = pbi_oth_amount_schedule
725+ ret ['pbi_oth_term' ] = model .surfaceplant .plant_lifetime .value
726+ ret ['pbi_oth_tax_fed' ] = 0.0 # Strictly exclude from Federal taxable gross income
727+ ret ['pbi_oth_tax_sta' ] = 0.0 # Strictly exclude from State taxable gross income
728+
729+ # Inject the combined array into SAM
730+ if any (rate > 0.0 for rate in ptc_fed_amount_schedule ):
731+ ret ['ptc_fed_amount' ] = ptc_fed_amount_schedule
732+ ret ['ptc_fed_term' ] = model .surfaceplant .plant_lifetime .value
733+ ret ['ptc_fed_escal' ] = 0.0 # Escalation is already manually calculated in the array above
690734
691735 # 'Property Tax Rate'
692736 geophires_ptr_tenths = Decimal (econ .PTR .value )
@@ -758,22 +802,6 @@ def _price_vector(price_value: list[float]) -> list[float | str]:
758802 )
759803 ret .append (carbon_revenue_source )
760804
761- if econ .DoSDACGTCalculations .value :
762- # Pad the scalar price to match the full timeline array length required by SAM formatting
763- sdac_price_array = [0.0 ] * _pre_revenue_years_count (model ) + [
764- model .sdacgteconomics .carbon_credit_price .value
765- ] * model .surfaceplant .plant_lifetime .value
766-
767- sdac_revenue_source = CapacityPaymentRevenueSource (
768- name = 'S-DAC-GT Carbon credits' ,
769- revenue_usd = [round (it ) for it in model .sdacgteconomics .CarbonRevenue .value ],
770- price_label = f'S-DAC-GT Carbon credit price ({ model .sdacgteconomics .carbon_credit_price .CurrentUnits .value } )' ,
771- price = _price_vector (sdac_price_array ),
772- amount_provided_label = f'S-DAC-GT Carbon Extracted ({ model .sdacgteconomics .CarbonExtractedAnnually .CurrentUnits .value } )' ,
773- amount_provided = model .sdacgteconomics .CarbonExtractedAnnually .value ,
774- )
775- ret .append (sdac_revenue_source )
776-
777805 def _get_revenue_usd_series (econ_revenue_output : OutputParameter ) -> Iterable [float ]:
778806 return [
779807 round (it )
0 commit comments