Skip to content
This repository was archived by the owner on Mar 27, 2025. It is now read-only.

Commit 61b864f

Browse files
Co2 accounting refactor (#197)
* update submodule * set up all required dataframes for new emission accounting * switch to new emissiosn accounting * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * simplify CCS accounting * handle CHP emissions correctly * some comments * rename co2 getter and delete old function * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 9193691 commit 61b864f

File tree

3 files changed

+153
-103
lines changed

3 files changed

+153
-103
lines changed

config/config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
# docs in https://pypsa-eur.readthedocs.io/en/latest/configuration.html#run
66
run:
7-
prefix: 240919_nep_2021_costs
7+
prefix: 240919_emissions_accounting
88
name:
99
# - CurrentPolicies
1010
- KN2045_Bal_v4

workflow/scripts/export_ariadne_variables.py

Lines changed: 151 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -2386,7 +2386,7 @@ def get_emissions(n, region, _energy_totals):
23862386
.multiply(t2Mt)
23872387
)
23882388

2389-
var["Emissions|CO2"] = co2_emissions.sum() - co2_atmosphere_withdrawal.sum()
2389+
var["Emissions|CO2|Model"] = co2_emissions.sum() - co2_atmosphere_withdrawal.sum()
23902390

23912391
co2_storage = (
23922392
n.statistics.supply(bus_carrier="co2 stored", **kwargs)
@@ -2400,19 +2400,94 @@ def get_emissions(n, region, _energy_totals):
24002400
assert co2_storage.get("co2 stored", 0) < 1.0
24012401
co2_storage.drop("co2 stored", inplace=True, errors="ignore")
24022402

2403+
try:
2404+
total_ccs = (
2405+
n.statistics.supply(bus_carrier="co2 sequestered", **kwargs)
2406+
.filter(like=region)
2407+
.get("Link")
2408+
.groupby("carrier")
2409+
.sum()
2410+
.multiply(t2Mt)
2411+
.sum()
2412+
)
2413+
except AttributeError: # no sequestration in 2020 -> NoneType
2414+
total_ccs = 0.0
2415+
2416+
# CCU is regarded as emissions
2417+
ccs_fraction = total_ccs / co2_storage.sum()
2418+
ccu_fraction = 1 - ccs_fraction
2419+
2420+
common_index_emitters = co2_emissions.index.intersection(co2_storage.index)
2421+
2422+
co2_emissions.loc[common_index_emitters] += co2_storage.loc[
2423+
common_index_emitters
2424+
].multiply(ccu_fraction)
2425+
2426+
common_index_withdrawals = co2_atmosphere_withdrawal.index.intersection(
2427+
co2_storage.index
2428+
)
2429+
2430+
co2_atmosphere_withdrawal.loc[common_index_withdrawals] -= co2_storage.loc[
2431+
common_index_withdrawals
2432+
].multiply(ccu_fraction)
2433+
2434+
assert isclose(
2435+
co2_emissions.sum() - co2_atmosphere_withdrawal.sum(),
2436+
var["Emissions|CO2|Model"] + co2_storage.sum() * ccu_fraction,
2437+
)
2438+
2439+
# Now repeat the same for the CHP emissions
2440+
24032441
CHP_emissions = (
2404-
n.statistics.supply(bus_carrier="co2", **kwargs)
2405-
.filter(like=region)
2406-
.filter(like="CHP")
2407-
.multiply(t2Mt)
2442+
(
2443+
n.statistics.supply(bus_carrier="co2", **kwargs)
2444+
.filter(like=region)
2445+
.filter(like="CHP")
2446+
.multiply(t2Mt)
2447+
)
2448+
.groupby(["name", "carrier"])
2449+
.sum()
24082450
)
24092451

2410-
# exclude waste CHPs because they are accounted separately
2411-
CHP_emissions = CHP_emissions[
2412-
~CHP_emissions.index.get_level_values("carrier").str.contains("waste")
2413-
]
2452+
CHP_atmosphere_withdrawal = (
2453+
(
2454+
n.statistics.withdrawal(bus_carrier="co2", **kwargs)
2455+
.filter(like=region)
2456+
.filter(like="CHP")
2457+
.multiply(t2Mt)
2458+
)
2459+
.groupby(["name", "carrier"])
2460+
.sum()
2461+
)
2462+
2463+
CHP_storage = (
2464+
(
2465+
n.statistics.supply(bus_carrier="co2 stored", **kwargs)
2466+
.filter(like=region)
2467+
.filter(like="CHP")
2468+
.multiply(t2Mt)
2469+
)
2470+
.groupby(["name", "carrier"])
2471+
.sum()
2472+
)
2473+
2474+
# CCU is regarded as emissions
2475+
2476+
common_index_emitters = CHP_emissions.index.intersection(CHP_storage.index)
2477+
2478+
CHP_emissions.loc[common_index_emitters] += CHP_storage.loc[
2479+
common_index_emitters
2480+
].multiply(ccu_fraction)
2481+
2482+
common_index_withdrawals = CHP_atmosphere_withdrawal.index.intersection(
2483+
CHP_storage.index
2484+
)
24142485

2415-
## Account for carbon neutral fuels (e-fuels, biogas, ...)
2486+
CHP_atmosphere_withdrawal.loc[common_index_withdrawals] -= CHP_storage.loc[
2487+
common_index_withdrawals
2488+
].multiply(ccu_fraction)
2489+
2490+
## E-fuels are assumed to be carbon neutral
24162491

24172492
oil_techs = [
24182493
"HVC to air",
@@ -2428,14 +2503,15 @@ def get_emissions(n, region, _energy_totals):
24282503
"urban central oil CHP",
24292504
]
24302505

2506+
# multiply by fossil fraction to disregard e-fuel emissions
2507+
24312508
oil_fossil_fraction = _get_oil_fossil_fraction(n, region)
2432-
# Assuming that efuel emissions are generated at the production site
2433-
var["Emissions|CO2|Energy|Production|From Liquids"] = co2_emissions.loc[
2509+
2510+
# This variable is not in the database, but it might be useful for double checking the totals
2511+
var["Emissions|CO2|Efuels|Liquids"] = co2_emissions.loc[
24342512
co2_emissions.index.isin(oil_techs)
24352513
].sum() * (1 - oil_fossil_fraction)
24362514

2437-
# Fossil fuel emissions are generated where they get burned
2438-
24392515
co2_emissions.loc[co2_emissions.index.isin(oil_techs)] *= oil_fossil_fraction
24402516

24412517
CHP_emissions.loc[
@@ -2457,17 +2533,32 @@ def get_emissions(n, region, _energy_totals):
24572533

24582534
gas_fractions = _get_gas_fractions(n, region)
24592535

2460-
var["Emissions|CO2|Energy|Production|From Gases"] = co2_emissions.loc[
2536+
var["Emissions|CO2|Efuels|Gases"] = co2_emissions.loc[
24612537
co2_emissions.index.isin(gas_techs)
24622538
].sum() * (1 - gas_fractions["Natural Gas"])
2539+
24632540
co2_emissions.loc[co2_emissions.index.isin(gas_techs)] *= gas_fractions[
24642541
"Natural Gas"
24652542
]
24662543
CHP_emissions.loc[
24672544
CHP_emissions.index.get_level_values("carrier").isin(gas_techs)
24682545
] *= gas_fractions["Natural Gas"]
24692546

2470-
# TODO Methanol
2547+
# TODO Methanol?
2548+
2549+
# Emissions in DE are:
2550+
2551+
var["Emissions|CO2"] = co2_emissions.sum() - co2_atmosphere_withdrawal.sum()
2552+
2553+
assert isclose(
2554+
var["Emissions|CO2"],
2555+
var["Emissions|CO2|Model"]
2556+
+ co2_storage.sum() * ccu_fraction
2557+
- var["Emissions|CO2|Efuels|Liquids"]
2558+
- var["Emissions|CO2|Efuels|Gases"],
2559+
)
2560+
2561+
# Split CHP emissions between electricity and heat sectors
24712562

24722563
CHP_E_to_H = (
24732564
n.links.loc[CHP_emissions.index.get_level_values("name")].efficiency
@@ -2476,80 +2567,37 @@ def get_emissions(n, region, _energy_totals):
24762567

24772568
CHP_E_fraction = CHP_E_to_H * (1 / (CHP_E_to_H + 1))
24782569

2479-
ccs_carriers = co2_storage.index.intersection(co2_atmosphere_withdrawal.index)
2480-
fossil_cc_carriers = co2_storage.index.difference(co2_atmosphere_withdrawal.index)
2570+
negative_CHP_E_to_H = (
2571+
n.links.loc[CHP_atmosphere_withdrawal.index.get_level_values("name")].efficiency
2572+
/ n.links.loc[
2573+
CHP_atmosphere_withdrawal.index.get_level_values("name")
2574+
].efficiency2
2575+
)
24812576

2482-
negative_cc = co2_storage.reindex(ccs_carriers)
2483-
fossil_cc = co2_storage.reindex(fossil_cc_carriers)
2577+
negative_CHP_E_fraction = negative_CHP_E_to_H * (1 / (negative_CHP_E_to_H + 1))
24842578

2485-
assert isclose(fossil_cc.sum() + negative_cc.sum(), co2_storage.sum())
2579+
# separate waste CHPs, because they are accounted differently
2580+
waste_CHP_emissions = CHP_emissions.filter(like="waste")
2581+
CHP_emissions = CHP_emissions.drop(waste_CHP_emissions.index)
24862582

2487-
try:
2488-
total_ccs = (
2489-
n.statistics.supply(bus_carrier="co2 sequestered", **kwargs)
2490-
.filter(like=region)
2491-
.get("Link")
2492-
.groupby("carrier")
2493-
.sum()
2494-
.multiply(t2Mt)
2495-
.sum()
2496-
)
2497-
except AttributeError: # no sequestration in 2020 -> NoneType
2498-
total_ccs = 0.0
2583+
# It would be interesting to relate the Emissions|CO2|Model to Emissions|CO2 reported to the DB by considering imports of carbon, e.g., (exports_oil_renew - imports_oil_renew) * 0.2571 * t2Mt + (exports_gas_renew - imports_gas_renew) * 0.2571 * t2Mt + (exports_meoh - imports_meoh) / 4.0321 * t2Mt
2584+
# Then it would be necessary to consider negative carbon from solid biomass imports as well
2585+
# Actually we might have to include solid biomass imports in the co2 constraints as well
24992586

2500-
negative_ccs = total_ccs - fossil_cc.sum()
2501-
2502-
co2_negative_emissions = negative_cc.multiply(negative_ccs / negative_cc.sum())
2503-
2504-
if negative_ccs < 0:
2505-
co2_negative_emissions *= 0
2506-
# If not enough CO2 is captured, than additional emissions occur
2507-
fossil_cc_emissions = -fossil_cc.multiply(negative_ccs / fossil_cc.sum())
2508-
# All captured fossil should be sequestered for e-fuels to be carbon neutral
2509-
# If this warning appears repeatedly we will need to add a hard constraint
2510-
print("WARNING! Not all CO2 capture from fossil sources is captured!!!")
2511-
print("total_ccs - fossil_cc: ", total_ccs - fossil_cc.sum())
2512-
# TODO what to do with fossil_cc_emissions???
2513-
if (n.links.build_year.max() == 2045) and (
2514-
total_ccs - fossil_cc.sum() > 1
2515-
): # > 1 for numerical errors
2516-
raise Exception("Not enough CCS in 2045!")
2517-
2518-
if not co2_atmosphere_withdrawal.get("urban central solid biomass CHP CC"):
2519-
biomass_CHP_correction_factor = 0
2520-
else:
2521-
biomass_CHP_correction_factor = min(
2522-
1, # Can not be > 1, taking minimum to avoid numerical errors
2523-
co2_negative_emissions.get("urban central solid biomass CHP CC")
2524-
/ co2_atmosphere_withdrawal.get("urban central solid biomass CHP CC"),
2525-
)
2526-
2527-
negative_CHP_emissions = (
2528-
n.statistics.withdrawal(bus_carrier="co2", **kwargs)
2529-
.filter(like=region)
2530-
.filter(like="solid biomass CHP CC")
2531-
.multiply(t2Mt)
2532-
.multiply( # Correcting for actual negative emissions
2533-
biomass_CHP_correction_factor
2534-
)
2587+
assert isclose(
2588+
co2_emissions.filter(like="CHP").sum(),
2589+
CHP_emissions.sum() + waste_CHP_emissions.sum(),
25352590
)
2536-
2537-
negative_CHP_E_to_H = (
2538-
n.links.loc[negative_CHP_emissions.index.get_level_values("name")].efficiency
2539-
/ n.links.loc[negative_CHP_emissions.index.get_level_values("name")].efficiency2
2591+
assert isclose(
2592+
co2_atmosphere_withdrawal.filter(like="CHP").sum(),
2593+
CHP_atmosphere_withdrawal.sum(),
25402594
)
25412595

2542-
negative_CHP_E_fraction = negative_CHP_E_to_H * (1 / (negative_CHP_E_to_H + 1))
2543-
25442596
var["Carbon Sequestration"] = total_ccs
25452597

2546-
var["Carbon Sequestration|DACCS"] = var["Carbon Sequestration"] * (
2547-
co2_storage.filter(like="DAC").sum() / co2_storage.sum()
2548-
)
2598+
var["Carbon Sequestration|DACCS"] = co2_storage.filter(like="DAC").sum()
25492599

2550-
var["Carbon Sequestration|BECCS"] = var["Carbon Sequestration"] * (
2551-
co2_storage.filter(like="bio").sum() / co2_storage.sum()
2552-
)
2600+
var["Carbon Sequestration|BECCS"] = co2_storage.filter(like="bio").sum()
25532601

25542602
var["Carbon Sequestration|Other"] = (
25552603
var["Carbon Sequestration"]
@@ -2568,10 +2616,11 @@ def get_emissions(n, region, _energy_totals):
25682616
]
25692617
).sum() + co2_emissions.get("industry methanol", 0)
25702618
# process emissions is mainly cement, methanol is used for chemicals
2619+
# TODO where should the methanol go?
25712620

25722621
var["Emissions|CO2|Energy|Demand|Industry"] = co2_emissions.reindex(
25732622
["gas for industry", "gas for industry CC", "coal for industry"]
2574-
).sum() - co2_negative_emissions.get(
2623+
).sum() - co2_atmosphere_withdrawal.get(
25752624
"solid biomass for industry CC",
25762625
0,
25772626
)
@@ -2658,19 +2707,17 @@ def get_emissions(n, region, _energy_totals):
26582707

26592708
var["Emissions|CO2|Energy|Supply|Electricity"] = (
26602709
var["Emissions|Gross Fossil CO2|Energy|Supply|Electricity"]
2661-
- negative_CHP_emissions.multiply(negative_CHP_E_fraction).values.sum()
2710+
- CHP_atmosphere_withdrawal.multiply(negative_CHP_E_fraction).values.sum()
26622711
)
26632712

26642713
var["Emissions|Gross Fossil CO2|Energy|Supply|Heat"] = (
2665-
co2_emissions.filter(like="urban central")
2666-
.filter(like="boiler") # in 2020 there might be central oil boilers?!
2667-
.sum()
2714+
co2_emissions.filter(like="urban central").filter(like="boiler").sum()
26682715
+ CHP_emissions.multiply(1 - CHP_E_fraction).values.sum()
26692716
)
26702717

26712718
var["Emissions|CO2|Energy|Supply|Heat"] = (
26722719
var["Emissions|Gross Fossil CO2|Energy|Supply|Heat"]
2673-
- negative_CHP_emissions.multiply(1 - negative_CHP_E_fraction).values.sum()
2720+
- CHP_atmosphere_withdrawal.multiply(1 - negative_CHP_E_fraction).values.sum()
26742721
)
26752722

26762723
var["Emissions|CO2|Energy|Supply|Electricity and Heat"] = (
@@ -2682,24 +2729,18 @@ def get_emissions(n, region, _energy_totals):
26822729
"Emissions|Gross Fossil CO2|Energy|Supply|Hydrogen"
26832730
] = co2_emissions.filter(like="SMR").sum()
26842731

2685-
var["Emissions|CO2|Energy|Supply|Gases"] = (-1) * co2_negative_emissions.filter(
2732+
var["Emissions|CO2|Energy|Supply|Gases"] = (-1) * co2_atmosphere_withdrawal.filter(
26862733
like="biogas to gas"
26872734
).sum()
26882735

2689-
var["Emissions|CO2|Supply|Non-Renewable Waste"] = co2_emissions.reindex(
2690-
[
2691-
"HVC to air",
2692-
"waste CHP",
2693-
"waste CHP CC",
2694-
]
2695-
).sum()
2736+
var["Emissions|CO2|Supply|Non-Renewable Waste"] = (
2737+
co2_emissions.get("HVC to air").sum() + waste_CHP_emissions.sum()
2738+
)
26962739

26972740
var["Emissions|CO2|Energy|Supply|Liquids and Gases"] = var[
26982741
"Emissions|CO2|Energy|Supply|Liquids"
26992742
] = co2_emissions.get("oil refining", 0)
27002743

2701-
# var["Emissions|CO2|Energy|Supply|Gases"] + \
2702-
27032744
var["Emissions|CO2|Energy|Supply"] = (
27042745
var["Emissions|CO2|Energy|Supply|Gases"]
27052746
+ var["Emissions|CO2|Energy|Supply|Hydrogen"]
@@ -2733,21 +2774,20 @@ def get_emissions(n, region, _energy_totals):
27332774
var["Emissions|CO2|Energy and Industrial Processes"]
27342775
+ var["Emissions|CO2|Energy|Demand|Bunkers"]
27352776
+ var["Emissions|CO2|Supply|Non-Renewable Waste"]
2736-
- co2_negative_emissions.get("DAC", 0)
2737-
+ var["Emissions|CO2|Energy|Production|From Liquids"]
2738-
+ var["Emissions|CO2|Energy|Production|From Gases"]
2739-
- co2_atmosphere_withdrawal.subtract(co2_negative_emissions).sum()
2777+
- co2_atmosphere_withdrawal.get("DAC", 0)
27402778
)
2779+
27412780
print(
27422781
"Differences in accounting for CO2 emissions:",
27432782
emission_difference,
27442783
)
27452784

2746-
assert emission_difference < 1e-2 # Improve numerical stability
2785+
assert abs(emission_difference) < 1e-5
27472786

27482787
return var
27492788

27502789

2790+
27512791
# functions for prices
27522792
def get_nodal_flows(n, bus_carrier, region, query="index == index or index != index"):
27532793
"""
@@ -3879,6 +3919,16 @@ def get_export_import_links(n, region, carriers):
38793919

38803920
# TODO add methanol trade, renewable gas trade
38813921

3922+
exports_meoh, imports_meoh = get_export_import_links(n, region, ["methanol"])
3923+
3924+
var["Trade|Secondary Energy|Methanol|Hydrogen|Volume"] = (
3925+
exports_meoh - imports_meoh
3926+
) * MWh2PJ
3927+
3928+
var["Trade|Secondary Energy|Methanol|Hydrogen|Gross Import|Volume"] = (
3929+
imports_meoh * MWh2PJ
3930+
)
3931+
38823932
# Trade|Primary Energy|Coal|Volume
38833933
# Trade|Primary Energy|Gas|Volume
38843934

0 commit comments

Comments
 (0)