Skip to content

Commit c1af3f0

Browse files
Calculate S-DAC carbon revenue (addresses NatLabRockies#341)
1 parent 0862596 commit c1af3f0

5 files changed

Lines changed: 172 additions & 113 deletions

File tree

src/geophires_x/Economics.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2901,6 +2901,18 @@ def Calculate(self, model: Model) -> None:
29012901
if self.DoSDACGTCalculations.value:
29022902
model.sdacgteconomics.Calculate(model)
29032903

2904+
# Consolidate S-DAC-GT CAPEX and OPEX into the main plant ledgers
2905+
max_carbon_capacity_tonnes = np.max(model.sdacgteconomics.CarbonExtractedAnnually.value)
2906+
sdac_overnight_capex_musd = (
2907+
model.sdacgteconomics.CAPEX.value * model.sdacgteconomics.CAPEX_mult.value * max_carbon_capacity_tonnes) / 1_000_000.0
2908+
self.CCap.value += sdac_overnight_capex_musd
2909+
2910+
avg_carbon_extracted_tonnes = np.average(model.sdacgteconomics.CarbonExtractedAnnually.value)
2911+
sdac_annual_opex_musd = ((
2912+
model.sdacgteconomics.OPEX.value + model.sdacgteconomics.storage.value + model.sdacgteconomics.transport.value)
2913+
* avg_carbon_extracted_tonnes) / 1_000_000.0
2914+
self.Coam.value += sdac_annual_opex_musd
2915+
29042916
self.calculate_cashflow(model)
29052917

29062918
# Calculate more financial values using numpy financials
@@ -3723,6 +3735,13 @@ def calculate_cashflow(self, model: Model) -> None:
37233735
self.TotalRevenue.value[i] = self.TotalRevenue.value[i] + self.CarbonRevenue.value[i]
37243736
#self.TotalCummRevenue.value[i] = self.TotalCummRevenue.value[i] + self.CarbonCummCashFlow.value[i]
37253737

3738+
if self.DoSDACGTCalculations.value:
3739+
for i in range(model.surfaceplant.construction_years.value,
3740+
model.surfaceplant.plant_lifetime.value + model.surfaceplant.construction_years.value,
3741+
1):
3742+
sdac_index = i - model.surfaceplant.construction_years.value
3743+
self.TotalRevenue.value[i] += (model.sdacgteconomics.CarbonRevenue.value[sdac_index] / 1_000_000.0)
3744+
37263745
# for the sake of display, insert zeros at the beginning of the pricing arrays
37273746
for i in range(0, model.surfaceplant.construction_years.value, 1):
37283747
self.ElecPrice.value.insert(0, 0.0)

src/geophires_x/EconomicsS_DAC_GT.py

Lines changed: 62 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,19 @@ 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(
223+
"S-DAC-GT Carbon Credit Price",
224+
value=180.0,
225+
DefaultValue=180.0,
226+
Min=0.0,
227+
Max=1000.0,
228+
UnitType=Units.COSTPERMASS,
229+
PreferredUnits=CostPerMassUnit.DOLLARSPERTONNE,
230+
CurrentUnits=CostPerMassUnit.DOLLARSPERTONNE,
231+
ErrMessage="assume default Carbon Credit Price (180 USD per tonne CO2)",
232+
ToolTipText="Carbon Credit or Market Price (USD per tonne CO2)"
233+
)
234+
self.ParameterDict[self.carbon_credit_price.Name] = self.carbon_credit_price
222235

223236
# local variable initiation
224237
# Capital Recovery Rate or Fixed Charge Factor - set initially for definitions
@@ -335,6 +348,21 @@ def __init__(self, model: Model):
335348
PreferredUnits=CostPerMassUnit.DOLLARSPERTONNE,
336349
CurrentUnits=CostPerMassUnit.DOLLARSPERTONNE
337350
)
351+
self.CarbonRevenue = OutputParameter(
352+
Name="Annual Carbon Revenue",
353+
UnitType=Units.CURRENCYFREQUENCY,
354+
PreferredUnits=CurrencyFrequencyUnit.DOLLARSPERYEAR,
355+
CurrentUnits=CurrencyFrequencyUnit.DOLLARSPERYEAR
356+
)
357+
self.OutputParameterDict[self.CarbonRevenue.Name] = self.CarbonRevenue
358+
359+
self.CarbonCummCashFlow = OutputParameter(
360+
Name="Cumulative Carbon Revenue",
361+
UnitType=Units.CURRENCY,
362+
PreferredUnits=CurrencyUnit.DOLLARS,
363+
CurrentUnits=CurrencyUnit.DOLLARS
364+
)
365+
self.OutputParameterDict[self.CarbonCummCashFlow.Name] = self.CarbonCummCashFlow
338366

339367
model.logger.info(f"Complete {str(__class__)}: {sys._getframe().f_code.co_name}")
340368

@@ -599,12 +627,13 @@ def Calculate(self, model: Model) -> None:
599627
# Convert from $/McF to $/kWh_th, but don't change any parameters value directly - it will throw off the rehydration
600628
NG_price = self.NG_price.value / self.NG_EnergyDensity.value
601629
NG_totalcost = self.therm.value * NG_price
602-
self.LCOH.value, self.kWh_e_per_kWh_th.value = self.geo_therm_cost(model.surfaceplant.electricity_cost_to_buy.value,
603-
self.CAPEX_mult.value, self.OPEX_mult.value,
604-
model.reserv.depth.value * 3280.84,
605-
np.average(model.wellbores.ProducedTemperature.value),
606-
model.wellbores.Tinj.value,
607-
model.wellbores.nprod.value * model.wellbores.prodwellflowrate.value)
630+
self.LCOH.value, self.kWh_e_per_kWh_th.value = self.geo_therm_cost(
631+
model.surfaceplant.electricity_cost_to_buy.value,
632+
self.CAPEX_mult.value, self.OPEX_mult.value,
633+
model.reserv.depth.value * 3280.84,
634+
np.average(model.wellbores.ProducedTemperature.value),
635+
model.wellbores.Tinj.value,
636+
model.wellbores.nprod.value * model.wellbores.prodwellflowrate.value)
608637
geothermal_totalcost = self.LCOH.value * self.therm.value
609638
co2_power = self.elec.value / 1000 * self.power_co2intensity.value
610639
co2_elec_heat = self.therm.value / 1000 * self.power_co2intensity.value
@@ -621,7 +650,8 @@ def Calculate(self, model: Model) -> None:
621650

622651
# calculate the net impact of S-DAC-GT on the annual production of the model
623652
avg_first_law_eff = np.average(model.surfaceplant.FirstLawEfficiency.value)
624-
self.tot_heat_energy_consumed_per_tonne.value = (self.elec.value / avg_first_law_eff) + self.therm.value # kWh_th/tonne
653+
self.tot_heat_energy_consumed_per_tonne.value = (
654+
self.elec.value / avg_first_law_eff) + self.therm.value # kWh_th/tonne
625655
self.tot_cost_per_tonne.value = CAPEX + self.OPEX.value + self.storage.value + self.transport.value # USD/tonne
626656
self.percent_thermal_energy_going_to_heat.value = self.therm.value / self.tot_heat_energy_consumed_per_tonne.value
627657

@@ -637,18 +667,22 @@ def Calculate(self, model: Model) -> None:
637667
# That then gives us the revenue, since we have a carbon price model
638668
# We can also get annual cash flow from it.
639669
for i in range(0, model.surfaceplant.plant_lifetime.value, 1):
640-
self.CarbonExtractedAnnually.value[i] = (self.EnergySplit.value * model.surfaceplant.HeatkWhExtracted.value[i]) / self.tot_heat_energy_consumed_per_tonne.value
670+
self.CarbonExtractedAnnually.value[i] = (self.EnergySplit.value * model.surfaceplant.HeatkWhExtracted.value[
671+
i]) / self.tot_heat_energy_consumed_per_tonne.value
641672
if i == 0:
642673
self.S_DAC_GTCummCarbonExtracted.value[i] = self.CarbonExtractedAnnually.value[i]
643674
else:
644-
self.S_DAC_GTCummCarbonExtracted.value[i] = self.S_DAC_GTCummCarbonExtracted.value[i - 1] + self.CarbonExtractedAnnually.value[i]
675+
self.S_DAC_GTCummCarbonExtracted.value[i] = self.S_DAC_GTCummCarbonExtracted.value[i - 1] + \
676+
self.CarbonExtractedAnnually.value[i]
645677
self.CarbonExtractedTotal.value = self.CarbonExtractedTotal.value + self.CarbonExtractedAnnually.value[i]
646678
self.S_DAC_GTAnnualCost.value[i] = self.CarbonExtractedAnnually.value[i] * self.tot_cost_per_tonne.value
647679
if i == 0:
648680
self.S_DAC_GTCummCashFlow.value[i] = self.S_DAC_GTAnnualCost.value[i]
649681
else:
650-
self.S_DAC_GTCummCashFlow.value[i] = self.S_DAC_GTCummCashFlow.value[i - 1] + self.S_DAC_GTAnnualCost.value[i]
651-
self.CummCostPerTonne.value[i] = self.S_DAC_GTCummCashFlow.value[i] / self.S_DAC_GTCummCarbonExtracted.value[i]
682+
self.S_DAC_GTCummCashFlow.value[i] = self.S_DAC_GTCummCashFlow.value[i - 1] + \
683+
self.S_DAC_GTAnnualCost.value[i]
684+
self.CummCostPerTonne.value[i] = self.S_DAC_GTCummCashFlow.value[i] / \
685+
self.S_DAC_GTCummCarbonExtracted.value[i]
652686

653687
# We need to update the heat and electricity generated because we have consumed
654688
# some (all) of it to do the capture, so when they get used in the final economic calculation (below),
@@ -657,28 +691,28 @@ def Calculate(self, model: Model) -> None:
657691
if model.surfaceplant.enduse_option.value is not EndUseOptions.HEAT:
658692
# all these end-use options have an electricity generation component
659693
model.surfaceplant.TotalkWhProduced.value[i] = model.surfaceplant.TotalkWhProduced.value[i] - (
660-
self.CarbonExtractedAnnually.value[i] * self.elec.value)
694+
self.CarbonExtractedAnnually.value[i] * self.elec.value)
661695
model.surfaceplant.NetkWhProduced.value[i] = model.surfaceplant.NetkWhProduced.value[i] - (
662-
self.CarbonExtractedAnnually.value[i] * self.elec.value)
696+
self.CarbonExtractedAnnually.value[i] * self.elec.value)
663697
if model.surfaceplant.enduse_option.value is not EndUseOptions.ELECTRICITY:
664698
model.surfaceplant.HeatkWhProduced.value[i] = model.surfaceplant.HeatkWhProduced.value[i] - (
665-
self.CarbonExtractedAnnually.value[i] * self.therm.value)
699+
self.CarbonExtractedAnnually.value[i] * self.therm.value)
666700
else:
667701
# all the end-use option of direct-use only component
668702
model.surfaceplant.HeatkWhProduced.value[i] = (model.surfaceplant.HeatkWhProduced.value[i] -
669-
(self.CarbonExtractedAnnually.value[i] * self.therm.value))
670-
671-
# FIXME TODO https://github.com/NREL/GEOPHIRES-X/issues/341?title=S-DAC+does+not+calculate+carbon+revenue
672-
# Build a revenue generation model for the carbon capture, assuming the capture is being sequestered and that
673-
# there is some sort of credit involved for doing that sequestering
674-
# note that there may already be values in the CarbonRevenue array, so we need to
675-
# add to them, not just set them. If there isn't values, there, the array will be filed with zeros, so adding won't be a problem
676-
#total_duration = model.surfaceplant.plant_lifetime.value
677-
#for i in range(0, total_duration, 1):
678-
# model.sdacgteconomics.CarbonRevenue.value[i] = (model.sdacgteconomics.CarbonRevenue.value[i] +
679-
# (self.CarbonExtractedAnnually.value[i] * model.economics.CarbonPrice.value[i]))
680-
# if i > 0:
681-
# model.economics.CarbonCummCashFlow.value[i] = model.economics.CarbonCummCashFlow.value[i - 1] + model.economics.CarbonRevenue.value[i]
703+
(self.CarbonExtractedAnnually.value[
704+
i] * self.therm.value))
705+
706+
# Calculate Carbon Revenue based on S-DAC-GT specific credit price
707+
self.CarbonRevenue.value = [0.0] * model.surfaceplant.plant_lifetime.value
708+
self.CarbonCummCashFlow.value = [0.0] * model.surfaceplant.plant_lifetime.value
709+
710+
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
712+
if i == 0:
713+
self.CarbonCummCashFlow.value[i] = self.CarbonRevenue.value[i]
714+
else:
715+
self.CarbonCummCashFlow.value[i] = self.CarbonCummCashFlow.value[i - 1] + self.CarbonRevenue.value[i]
682716

683717
self._calculate_derived_outputs(model)
684-
model.logger.info(f'Complete {str(__class__)}: {sys._getframe().f_code.co_name}')
718+
model.logger.info(f'Complete {str(__class__)}: {sys._getframe().f_code.co_name}')

src/geophires_x/OutputsS_DAC_GT.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -81,21 +81,26 @@ def PrintOutputs(self, model) -> tuple:
8181
model.sdacgteconomics.S_DAC_GTCummCashFlow.value
8282
sdac_df[f'Cum. Cost Per Tonne ({model.sdacgteconomics.CummCostPerTonne.PreferredUnits.value})|:,.2f'] = \
8383
model.sdacgteconomics.CummCostPerTonne.value
84+
sdac_df[f'Carbon Revenue ({model.sdacgteconomics.CarbonRevenue.PreferredUnits.value})|:,.2f'] = \
85+
model.sdacgteconomics.CarbonRevenue.value
86+
sdac_df[f'Cum. Carbon Revenue ({model.sdacgteconomics.CarbonCummCashFlow.PreferredUnits.value})|:,.2f'] = \
87+
model.sdacgteconomics.CarbonCummCashFlow.value
8488

8589
f.write(NL)
8690
f.write(" **********************" + NL)
87-
f.write(" * S-DAC-GT PROFILE *" + NL)
91+
f.write(" * S-DAC-GT PROFILE *" + NL)
8892
f.write(" **********************" + NL)
89-
f.write("Year Carbon Cumm. Carbon S-DAC-GT S-DAC-GT Cumm. Cumm. Cost" + NL)
90-
f.write("Since Captured Captured Annual Cost Cash Flow Cost Per Tonne" + NL)
93+
f.write("Year Carbon Cumm. Carbon S-DAC-GT S-DAC-GT Cumm. Cumm. Cost Annual Carbon" + NL)
94+
f.write("Since Captured Captured Annual Cost Cash Flow Cost Per Tonne Revenue" + NL)
9195
f.write("Start ("+model.sdacgteconomics.CarbonExtractedAnnually.PreferredUnits.value +
9296
") ("+model.sdacgteconomics.S_DAC_GTCummCarbonExtracted.PreferredUnits.value +
9397
") ("+model.sdacgteconomics.S_DAC_GTAnnualCost.PreferredUnits.value +
9498
") ("+model.sdacgteconomics.S_DAC_GTCummCashFlow.PreferredUnits.value +
95-
") ("+model.sdacgteconomics.CummCostPerTonne.PreferredUnits.value + ")" +NL)
99+
") ("+model.sdacgteconomics.CummCostPerTonne.PreferredUnits.value +
100+
") (" + model.sdacgteconomics.CarbonRevenue.PreferredUnits.value + ")" + NL)
96101
i = 0
97102
for i in range(0, model.surfaceplant.plant_lifetime.value, 1):
98-
f.write(f" {i+1:3.0f} {model.sdacgteconomics.CarbonExtractedAnnually.value[i]:,.2f} {model.sdacgteconomics.S_DAC_GTCummCarbonExtracted.value[i]:,.2f} {model.sdacgteconomics.S_DAC_GTAnnualCost.value[i]:,.2f} {model.sdacgteconomics.S_DAC_GTCummCashFlow.value[i]:,.2f} {model.sdacgteconomics.CummCostPerTonne.value[i]:.2f}" + NL)
103+
f.write(f" {i+1:3.0f} {model.sdacgteconomics.CarbonExtractedAnnually.value[i]:,.2f} {model.sdacgteconomics.S_DAC_GTCummCarbonExtracted.value[i]:,.2f} {model.sdacgteconomics.S_DAC_GTAnnualCost.value[i]:,.2f} {model.sdacgteconomics.S_DAC_GTCummCashFlow.value[i]:,.2f} {model.sdacgteconomics.CummCostPerTonne.value[i]:.2f} {model.sdacgteconomics.CarbonRevenue.value[i]:,.2f}" + NL)
99104
i = i + 1
100105

101106
except BaseException as ex:
@@ -106,9 +111,9 @@ def PrintOutputs(self, model) -> tuple:
106111
print(msg)
107112
model.logger.critical(str(ex))
108113
model.logger.critical(msg)
109-
raise RuntimeError(msg, e)
114+
raise RuntimeError(msg, ex)
110115

111116
model.logger.info(f'Complete {str(__class__)}: {__name__}')
112117

113118
sdac_df = sdac_df.reset_index()
114-
return sdac_df, sdac_results
119+
return sdac_df, sdac_results

0 commit comments

Comments
 (0)