Skip to content

Commit aacd314

Browse files
WIP - adjusting S-DAC parameterization based on further research
1 parent 52aab3b commit aacd314

4 files changed

Lines changed: 236 additions & 192 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ src/geophires_docs/fervo_project_red-2026_graph-data-extraction.xcf
125125
/Useful sites for Sphinx docstrings.txt
126126
/.github/workflows/workflows.7z
127127
tmp.patch
128+
project-structure.txt
128129

129130
# Mypy Cache
130131
.mypy_cache/

src/geophires_x/EconomicsS_DAC_GT.py

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ def __init__(self, model: Model):
219219
ErrMessage="assume default Percent Energy Devoted To Process (50%)",
220220
ToolTipText="Percent Energy Devoted To Process (%)"
221221
)
222-
self.carbon_credit_price = floatParameter(
222+
self.carbon_credit_price = self.ParameterDict[self.carbon_credit_price.Name] = floatParameter(
223223
"S-DAC-GT Carbon Credit Price",
224224
value=180.0,
225225
DefaultValue=180.0,
@@ -231,7 +231,19 @@ def __init__(self, model: Model):
231231
ErrMessage="assume default Carbon Credit Price (180 USD per tonne CO2)",
232232
ToolTipText="Carbon Credit or Market Price (USD per tonne CO2)"
233233
)
234-
self.ParameterDict[self.carbon_credit_price.Name] = self.carbon_credit_price
234+
235+
self.carbon_credit_duration = self.ParameterDict[self.carbon_credit_duration.Name] = floatParameter(
236+
"S-DAC-GT Carbon Credit Duration",
237+
value=12.0,
238+
DefaultValue=12.0,
239+
Min=0.0,
240+
Max=100.0,
241+
UnitType=Units.TIME,
242+
PreferredUnits=TimeUnit.YEAR,
243+
CurrentUnits=TimeUnit.YEAR,
244+
ErrMessage="assume default Carbon Credit Duration (12 years)",
245+
ToolTipText="Duration for which the carbon credit can be claimed (e.g., 12 years for US 45Q)"
246+
)
235247

236248
# local variable initiation
237249
# Capital Recovery Rate or Fixed Charge Factor - set initially for definitions
@@ -511,6 +523,13 @@ def range_check(self) -> tuple:
511523
storage_max)
512524
return True, error_message
513525

526+
if not (self.carbon_credit_duration.Min
527+
<= self.carbon_credit_duration.value
528+
<= self.carbon_credit_duration.Max):
529+
error_message = "S-DAC-GT ERROR: Carbon Credit Duration should be between {} and {}".format(
530+
self.carbon_credit_duration.Min, self.carbon_credit_duration.Max)
531+
return True, error_message
532+
514533
return False, ""
515534

516535
def geo_therm_cost(self, power_cost: float, CAPEX_mult: float, OPEX_mult: float, depth: float,
@@ -703,16 +722,19 @@ def Calculate(self, model: Model) -> None:
703722
(self.CarbonExtractedAnnually.value[
704723
i] * self.therm.value))
705724

706-
# Calculate Carbon Revenue based on S-DAC-GT specific credit price
725+
# Calculate Carbon Revenue based on S-DAC-GT specific credit price and duration
707726
self.CarbonRevenue.value = [0.0] * model.surfaceplant.plant_lifetime.value
708727
self.CarbonCummCashFlow.value = [0.0] * model.surfaceplant.plant_lifetime.value
709728

710729
for i in range(0, model.surfaceplant.plant_lifetime.value, 1):
711-
self.CarbonRevenue.value[i] = self.CarbonExtractedAnnually.value[i] * self.carbon_credit_price.value
730+
# Enforce the parameterized statutory limit for tax credits
731+
applicable_price = self.carbon_credit_price.value if i < self.carbon_credit_duration.value else 0.0
732+
733+
self.CarbonRevenue.value[i] = self.CarbonExtractedAnnually.value[i] * applicable_price
712734
if i == 0:
713735
self.CarbonCummCashFlow.value[i] = self.CarbonRevenue.value[i]
714736
else:
715-
self.CarbonCummCashFlow.value[i] = self.CarbonCummCashFlow.value[i - 1] + self.CarbonRevenue.value[i]
716-
737+
self.CarbonCummCashFlow.value[i] = self.CarbonCummCashFlow.value[i - 1] + \
738+
self.CarbonRevenue.value[i]
717739
self._calculate_derived_outputs(model)
718740
model.logger.info(f'Complete {str(__class__)}: {sys._getframe().f_code.co_name}')

src/geophires_x/EconomicsSam.py

Lines changed: 50 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)