This notebook shows how to use bw_timex
with a cradle-to-grave case study of an electric vehicle. The case study is simplified, not meant to reflect the complexity of electric mobility but to demonstrate hot to use bw_timex
.
More information on the inner workings of bw_timex
can be found here.
import bw2data as bd
bd.projects
# bd.projects.set_current("timex_example_electric_vehicle")
Brightway2 projects manager with 34 objects, including: AB_test_exploring_EI39 CCL dev CCL_ch_hh_ei39_no_hybrid __test_abc_loopA__ __test_abc_loopA_with_biosphere__ __test_abc_loopA_with_biosphere_advanced__ __test_abc_loopA_with_biosphere_extra__ __test_medusa__ __test_tds__ biosphere_advanced Use `sorted(projects)` to get full list, `projects.report()` to get a report on all projects.
if 'bw25_premise_background_v2' not in bd.projects:
import bw2io as bi
bi.backup.restore_project_directory(fp= '/Users/ajakobs/Documents/prospective_dynamic_lca/brightway2-project-bw25_premise_background_v2-backup.26-March-2024-01-40PM.tar.gz',
overwrite_existing=True)
bd.projects.set_current('bw25_premise_background_v2')
Using Ecoinvent v3.9 and premise
, we created a set of prospective databases with projections for the future electricity sectors using the SSP2-RCP19 pathway from the IAM IMAGE. We selected this pathway to simply demonstrate some future development in this case study, and many other models and pathways are available.
In the documentation of premise
, you can find instructions for the creation of prospective background databases.
db_2020 = bd.Database("db_2020")
db_2030 = bd.Database("db_2030")
db_2040 = bd.Database("db_2040")
Let's create a new foreground database to store the EV process:
del bd.databases["foreground"] # to make sure we create the foreground from scratch
foreground = bd.Database("foreground")
foreground.write({})
Getting the input processes of the EV from the background database:
glider_production = bd.get_activity(("db_2020", "133b33cc867081af144475d62179286b"))
powertrain_production = bd.get_activity(
("db_2020", "f6d3f0b01e4a38c055e3c5c1356a4bba")
) # eol included
battery_production = bd.get_activity(("db_2020", "ba87aff6361d99be2636e8c59e55a5b2"))
electricity_production = bd.get_activity(
("db_2020", "fec93a95a9a84d7fa0ede9c3082bb79f")
)
glider_eol = bd.get_activity(("db_2020", "f8114e0ff375b3c6d72ccfa49f79e44d"))
battery_eol = bd.get_activity(("db_2020", "82ebcdf42e8512cbe00151dda6210d29"))
Creating the EV activity:
fg = bd.Database("foreground")
fg.new_node("EV_lifecycle", name="Electric vehicle, lifecycle", unit="unit").save()
ev_lifecycle = fg.get("EV_lifecycle")
Here are some parameter assumptions for the EV:
ELECTRICITY_CONSUMPTION = 0.2 # kWh/km
MILEAGE = 150_000 # km
LIFETIME = 16 # years
# Overall mass: 1200 kg
MASS_GLIDER = 840 # kg
MASS_POWERTRAIN = 80 # kg
MASS_BATTERY = 280 # kg
Based on these parameters, we create the following exchanges:
ev_lifecycle.new_edge(input=ev_lifecycle, amount=1, type="production").save() # production exchange
glider_production_exchange = ev_lifecycle.new_edge(
input=glider_production,
amount=MASS_GLIDER,
type="technosphere"
)
powertrain_production_exchange = ev_lifecycle.new_edge(
input=powertrain_production,
amount=MASS_POWERTRAIN,
type="technosphere"
)
battery_production_exchange = ev_lifecycle.new_edge(
input=battery_production,
amount=MASS_BATTERY,
type="technosphere"
)
electricity_production_exchange = ev_lifecycle.new_edge(
input=electricity_production,
amount=ELECTRICITY_CONSUMPTION * MILEAGE,
type="technosphere",
)
glider_eol_exchange = ev_lifecycle.new_edge(
input=glider_eol,
amount=MASS_GLIDER, # amount not negative as its not modeled as a "true" waste process
type="technosphere",
)
battery_eol_exchange = ev_lifecycle.new_edge(
input=battery_eol,
amount=-MASS_BATTERY,
type="technosphere"
)
We need to add the temporal distributions at the exchanges level.
See bw_temporalis documentation for more information on TemporalDistribution
. Timedelta, which we use below, describes relative temporal relations, e.g. 2 years earlier.
from bw_temporalis import TemporalDistribution, easy_timedelta_distribution
import numpy as np
td_production = easy_timedelta_distribution(
start=-4,
end=0,
resolution="Y", # "Y": "Years", "M": "Months", "D": "Days", "h": "Hours" etc.
steps=5,
kind="triangular", # available kinds: "triangular", "uniform", "normal"
param=-1,
)
td_use_phase = easy_timedelta_distribution(
start=0, # (inclusive)
end=LIFETIME, # (inclusive)
resolution="Y",
steps=(LIFETIME + 1), # Includes both start and end
kind="uniform",
)
td_eol = TemporalDistribution(
date=np.array([LIFETIME + 1], dtype="timedelta64[Y]"), amount=np.array([1]) # if you build a TD manually, make sure that length of date array == length of amount array, and the sum of elements in the amount array == 1
)
Let's explore what a TemporalDistribution
looks like:
print("td_production.date [s]: ", td_production.date)
print("td_production.amount [-]:", td_production.amount)
type(td_production)
td_production.date [s]: [-126227808 -94670856 -63113904 -31556952 0] td_production.amount [-]: [0. 0.16666667 0.33333333 0.5 0. ]
bw_temporalis.temporal_distribution.TemporalDistribution
We now add the temporal information to the inputs of our EV. We add temporal distributions to all exchanges, but you don't have to.
glider_production_exchange["temporal_distribution"] = td_production
glider_production_exchange.save()
powertrain_production_exchange["temporal_distribution"] = td_production
powertrain_production_exchange.save()
battery_production_exchange["temporal_distribution"] = td_production
battery_production_exchange.save()
electricity_production_exchange["temporal_distribution"] = td_use_phase
electricity_production_exchange.save()
glider_eol_exchange["temporal_distribution"] = td_eol
glider_eol_exchange.save()
battery_eol_exchange["temporal_distribution"] = td_eol
battery_eol_exchange.save()
bw_timex
¶As usual, we need to select a method:
method = ("EF v3.1", "climate change", "global warming potential (GWP100)")
bw_timex
needs to know the representative time of the databases:
from datetime import datetime
database_date_dict = {
"db_2020": datetime.strptime("2020", "%Y"),
"db_2030": datetime.strptime("2030", "%Y"),
"db_2040": datetime.strptime("2040", "%Y"),
"foreground": "dynamic", # flag databases that should be temporally distributed with "dynamic"
}
Now, we can instantiate a TimexLCA
. It has a similar structure as bw2calc.LCA
, but with the additional argument database_date_dict
.
Not sure about the required inputs? Check the documentation using ?
from bw_timex import TimexLCA
TimexLCA?
Init signature: TimexLCA(demand: dict, method: tuple, database_date_dict: dict = None) -> None Docstring: Class to perform time-explicit LCA calculations. A TimexLCA retrieves the LCI of processes occuring at explicit points in time and relinks their technosphere exchanges to match the technology landscape at that point in time, while keeping track of the timing of the resulting emissions. As such, it combines prospective and dynamic LCA approaches. TimexLCA first calculates a static LCA, which informs a priority-first graph traversal. From the graph traversal, temporal relationships between exchanges and processes are derived. Based on the timing of the processes, bw_timex matches the processes at the intersection between foreground and background to the best available background databases. This temporal relinking is achieved by using datapackages to add new time-specific processes. The new processes and their exchanges to other technosphere processes or biosphere flows extent the technopshere and biosphere matrices. Temporal information of both processes and biosphere flows are retained, allowing for dynamic LCIA. TimexLCA calculates: 1) a static LCA score (`TimexLCA.base_lca.score`, same as `bw2calc.lca.score`), 2) a static time-explicit LCA score (`TimexLCA.static_score`), which links LCIs to the respective background databases but without additional temporal dynamics of the biosphere flows, 3) a dynamic time-explicit LCA score (`TimexLCA.dynamic_score`), with dynamic inventory and dynamic charaterization factors. These are provided for radiative forcing and GWP but can also be user-defined. Example ------- >>> demand = {('my_foreground_database', 'my_process'): 1} >>> method = ("some_method_family", "some_category", "some_method") >>> database_date_dict = { 'my_background_database_one': datetime.strptime("2020", "%Y"), 'my_background_database_two': datetime.strptime("2030", "%Y"), 'my_foreground_database':'dynamic' } >>> bw_timex = TimexLCA(demand, method, database_date_dict) >>> bw_timex.build_timeline() # you can pass many optional arguments here, also for the graph traversal >>> bw_timex.lci() >>> bw_timex.static_lcia() >>> print(bw_timex.static_score) >>> bw_timex.dynamic_lcia(metric="radiative_forcing") # different metrics can be used, e.g. "GWP", "radiative_forcing" >>> print(bw_timex.dynamic_score) Init docstring: Instantiating a `TimexLCA` object calculates a static LCA, initializes time mapping dicts for activities and biosphere flows, and stores useful subsets of ids in the node_id_collection_dict. Parameters ---------- demand : dict[object: float] The demand for which the LCA will be calculated. The keys can be Brightway `Node` instances, `(database, code)` tuples, or integer ids. method : tuple Tuple defining the LCIA method, such as `('foo', 'bar')` or default methods, such as `("EF v3.1", "climate change", "global warming potential (GWP100)")` database_date_dict : dict, optional Dictionary mapping database names to dates. File: ~/Documents/prospective_dynamic_lca/bw_timex/bw_timex/timex_lca.py Type: type Subclasses:
Let's create a TimexLCA
object for our EV life cycle:
tlca = TimexLCA({ev_lifecycle.key: 1}, method, database_date_dict)
Next, we build a timeline of the exchanges. To do this, we can call the build_timeline()
method, which does the graph traversal and creates a timeline dataframe from the results. The exchanges (rows of the dataframe) are aggregated to the resolution specified in the argument temporal_grouping
. There are also many more options to specify the timeline creation and graph traversal process. Here are the most important ones:
temporal_grouping
: temporal resolution to which processes will be aggregated,"year" (default), "month", "day" or "hour"interpolation_type
: How the best fitting background database is selected: "linear"(default), "closest"edge_filter_function
: Custom filter function specifying when to stop the graph traversal.cutoff
: stops graph traversal for nodes below this contribution to the static impact score.max_calc
: stops graph traversal if this number of nodes has been traversedFor all these options, we provide sensible default values. Of course you can always just check the docstrings to see all your options and our assumptions for default values.
So, let's build the timeline:
tlca.build_timeline()
/Users/ajakobs/Documents/prospective_dynamic_lca/bw_timex/bw_timex/timex_lca.py:186: UserWarning: No edge filter function provided. Skipping all edges within background databases. warnings.warn(
Starting graph traversal Calculation count: 7
/Users/ajakobs/Documents/prospective_dynamic_lca/bw_timex/bw_timex/timeline_builder.py:482: Warning: Reference date 2041-01-01 00:00:00 is higher than all provided dates. Data will be taken from the closest lower year. warnings.warn(
date_producer | producer_name | date_consumer | consumer_name | amount | interpolation_weights | |
---|---|---|---|---|---|---|
0 | 2021-01-01 | market for glider, passenger car | 2024-01-01 | Electric vehicle, lifecycle | 140.0 | {'db_2020': 0.8998083766767041, 'db_2030': 0.1... |
1 | 2021-01-01 | battery production, Li-ion, LiMn2O4, rechargea... | 2024-01-01 | Electric vehicle, lifecycle | 46.666667 | {'db_2020': 0.8998083766767041, 'db_2030': 0.1... |
2 | 2021-01-01 | market for powertrain, for electric passenger car | 2024-01-01 | Electric vehicle, lifecycle | 13.333333 | {'db_2020': 0.8998083766767041, 'db_2030': 0.1... |
3 | 2022-01-01 | market for glider, passenger car | 2024-01-01 | Electric vehicle, lifecycle | 280.0 | {'db_2020': 0.7998905009581166, 'db_2030': 0.2... |
4 | 2022-01-01 | battery production, Li-ion, LiMn2O4, rechargea... | 2024-01-01 | Electric vehicle, lifecycle | 93.333333 | {'db_2020': 0.7998905009581166, 'db_2030': 0.2... |
5 | 2022-01-01 | market for powertrain, for electric passenger car | 2024-01-01 | Electric vehicle, lifecycle | 26.666667 | {'db_2020': 0.7998905009581166, 'db_2030': 0.2... |
6 | 2023-01-01 | market for glider, passenger car | 2024-01-01 | Electric vehicle, lifecycle | 420.0 | {'db_2020': 0.6999726252395291, 'db_2030': 0.3... |
7 | 2023-01-01 | battery production, Li-ion, LiMn2O4, rechargea... | 2024-01-01 | Electric vehicle, lifecycle | 140.0 | {'db_2020': 0.6999726252395291, 'db_2030': 0.3... |
8 | 2023-01-01 | market for powertrain, for electric passenger car | 2024-01-01 | Electric vehicle, lifecycle | 40.0 | {'db_2020': 0.6999726252395291, 'db_2030': 0.3... |
9 | 2024-01-01 | market group for electricity, low voltage | 2024-01-01 | Electric vehicle, lifecycle | 1764.705882 | {'db_2020': 0.6000547495209416, 'db_2030': 0.3... |
10 | 2024-01-01 | Electric vehicle, lifecycle | 2024-01-01 | -1 | 1.0 | None |
11 | 2025-01-01 | market group for electricity, low voltage | 2024-01-01 | Electric vehicle, lifecycle | 1764.705882 | {'db_2020': 0.4998631261976457, 'db_2030': 0.5... |
12 | 2026-01-01 | market group for electricity, low voltage | 2024-01-01 | Electric vehicle, lifecycle | 1764.705882 | {'db_2020': 0.39994525047905827, 'db_2030': 0.... |
13 | 2027-01-01 | market group for electricity, low voltage | 2024-01-01 | Electric vehicle, lifecycle | 1764.705882 | {'db_2020': 0.3000273747604708, 'db_2030': 0.6... |
14 | 2028-01-01 | market group for electricity, low voltage | 2024-01-01 | Electric vehicle, lifecycle | 1764.705882 | {'db_2020': 0.20010949904188335, 'db_2030': 0.... |
15 | 2029-01-01 | market group for electricity, low voltage | 2024-01-01 | Electric vehicle, lifecycle | 1764.705882 | {'db_2020': 0.09991787571858746, 'db_2030': 0.... |
16 | 2030-01-01 | market group for electricity, low voltage | 2024-01-01 | Electric vehicle, lifecycle | 1764.705882 | {'db_2030': 1} |
17 | 2031-01-01 | market group for electricity, low voltage | 2024-01-01 | Electric vehicle, lifecycle | 1764.705882 | {'db_2030': 0.9000547645125958, 'db_2040': 0.0... |
18 | 2032-01-01 | market group for electricity, low voltage | 2024-01-01 | Electric vehicle, lifecycle | 1764.705882 | {'db_2030': 0.8001095290251916, 'db_2040': 0.1... |
19 | 2033-01-01 | market group for electricity, low voltage | 2024-01-01 | Electric vehicle, lifecycle | 1764.705882 | {'db_2030': 0.6998904709748084, 'db_2040': 0.3... |
20 | 2034-01-01 | market group for electricity, low voltage | 2024-01-01 | Electric vehicle, lifecycle | 1764.705882 | {'db_2030': 0.5999452354874042, 'db_2040': 0.4... |
21 | 2035-01-01 | market group for electricity, low voltage | 2024-01-01 | Electric vehicle, lifecycle | 1764.705882 | {'db_2030': 0.5, 'db_2040': 0.5} |
22 | 2036-01-01 | market group for electricity, low voltage | 2024-01-01 | Electric vehicle, lifecycle | 1764.705882 | {'db_2030': 0.4000547645125958, 'db_2040': 0.5... |
23 | 2037-01-01 | market group for electricity, low voltage | 2024-01-01 | Electric vehicle, lifecycle | 1764.705882 | {'db_2030': 0.29983570646221247, 'db_2040': 0.... |
24 | 2038-01-01 | market group for electricity, low voltage | 2024-01-01 | Electric vehicle, lifecycle | 1764.705882 | {'db_2030': 0.19989047097480828, 'db_2040': 0.... |
25 | 2039-01-01 | market group for electricity, low voltage | 2024-01-01 | Electric vehicle, lifecycle | 1764.705882 | {'db_2030': 0.0999452354874042, 'db_2040': 0.9... |
26 | 2040-01-01 | market group for electricity, low voltage | 2024-01-01 | Electric vehicle, lifecycle | 1764.705882 | {'db_2040': 1} |
27 | 2041-01-01 | market for used Li-ion battery | 2024-01-01 | Electric vehicle, lifecycle | -280.0 | {'db_2040': 1} |
28 | 2041-01-01 | market for manual dismantling of used electric... | 2024-01-01 | Electric vehicle, lifecycle | 840.0 | {'db_2040': 1} |
The interpolation weights in the timeline (right most column above) specify the share of the amount of an exchange to be sourced from the respective database.
None
means that the exchange is in the foreground supply chain, and not at the intersection with the background system.
Next, we calculate the time-explicit LCI. Here we have the option to expand the technosphere matrix, in case we want to have further access to the standard BW matrix based functions, or if we want just the dynamic inventory. The TimexLCA.lci()
function takes care of all the relinking if necessary, based on the information from the timeline. For the expanded matrix set expand_matrix=True (default).
# first try without the expanded matrix
tlca.lci(expand_technosphere=False)
tlca.static_lcia()
/Users/ajakobs/Documents/prospective_dynamic_lca/bw_timex/bw_timex/timex_lca.py:332: UserWarning: Currently the static lcia score can only be calculated if the expanded matrix has been built Please call TimexLCA.lci(expand_technosphere=True) first. warnings.warn("Currently the static lcia score can only be calculated if the expanded matrix has been built\
tlca.dynamic_biomatrix
<51009x29 sparse matrix of type '<class 'numpy.float64'>' with 68012 stored elements in Compressed Sparse Row format>
Taking a look at the dynamic_inventory
that was now created, we can see that it has more rows (emissions) than our usual biosphere3 flows. Instead of one row for each emission in the biosphere database we now get one row for each emission at each point in time.
tlca.dynamic_inventory
<51009x29 sparse matrix of type '<class 'numpy.float64'>' with 61373 stored elements in Compressed Sparse Row format>
tlca.dynamic_inventory_df
date | amount | flow | activity | |
---|---|---|---|---|
2627 | 2021-01-01 | 3.474853e+04 | 1909 | 25965 |
2626 | 2021-01-01 | 1.836769e+04 | 1909 | 25966 |
2625 | 2021-01-01 | 1.035863e+04 | 1909 | 25967 |
2474 | 2021-01-01 | 9.263883e+03 | 1756 | 25965 |
2473 | 2021-01-01 | 4.497189e+03 | 1756 | 25966 |
... | ... | ... | ... | ... |
59143 | 2041-01-01 | -1.852944e-08 | 2310 | 25993 |
58986 | 2041-01-01 | -3.563290e-08 | 2202 | 25992 |
59144 | 2041-01-01 | -1.161550e-07 | 2310 | 25992 |
60715 | 2041-01-01 | -4.034863e-01 | 4189 | 25993 |
58304 | 2041-01-01 | -3.801100e+00 | 1167 | 25992 |
61373 rows × 4 columns
# now let's calculate the lci with matrix expansion
tlca.lci(expand_technosphere=True)
tlca.static_lcia()
tlca.dynamic_biomatrix
<51009x72345 sparse matrix of type '<class 'numpy.float64'>' with 68012 stored elements in Compressed Sparse Row format>
tlca.dynamic_inventory
<51009x72345 sparse matrix of type '<class 'numpy.float64'>' with 61236 stored elements in Compressed Sparse Row format>
tlca.dynamic_inventory_df
date | amount | flow | activity | |
---|---|---|---|---|
2621 | 2021-01-01 | 3.474853e+04 | 1909 | 98281 |
2620 | 2021-01-01 | 1.836769e+04 | 1909 | 98282 |
2619 | 2021-01-01 | 1.035863e+04 | 1909 | 98283 |
2468 | 2021-01-01 | 9.263883e+03 | 1756 | 98281 |
2467 | 2021-01-01 | 4.497189e+03 | 1756 | 98282 |
... | ... | ... | ... | ... |
59008 | 2041-01-01 | -1.852944e-08 | 2310 | 98309 |
58851 | 2041-01-01 | -3.563290e-08 | 2202 | 98308 |
59009 | 2041-01-01 | -1.161550e-07 | 2310 | 98308 |
60578 | 2041-01-01 | -4.034863e-01 | 4189 | 98309 |
58169 | 2041-01-01 | -3.801100e+00 | 1167 | 98308 |
61236 rows × 4 columns
The standard, non-dynamic inventory has far less rows because the temporal resolution is missing. Looking at the timeline again, we see that we have processes at 21 different points in time, which should exactly match the ratio of the dimensions of our two inventories:
tlca.inventory
<2429x72345 sparse matrix of type '<class 'numpy.float64'>' with 920491 stored elements in Compressed Sparse Row format>
tlca.dynamic_inventory.shape[0] / tlca.inventory.shape[0]
21.0
If we are only interested in the new overall time-explicit scores and don't care about the timing of the emissions, we can set build_dynamic_biosphere=False
(default is True
), which saves time and memory. In that case, you only get the TimexLCA.inventory
, but not the TimexLCA.dynamic_inventory
.
In case the timing of emissions is not important, one can directly calculate the LCIA the "standard way" using static characterization methods. Per default, the following calculates the static lcia score based on the impact method chosen in the very beginning:
tlca.lci(build_dynamic_biosphere=False)
tlca.static_lcia()
tlca.score #kg CO2-eq
15697.67132351153
In addition to the standard static characterization, the time-explicit, dynamic inventory generated by a TimexLCA
allows for dynamic characterization. Users can provide their own dynamic characterization functions and link them to corresponding biosphere flows (see example on dynamic characterization), or use the ones we provide out of the box.
We provide two different metrics for dynamic LCIA of Climate Change: Radiative forcing [W/m2] and Global Warming Potential (GWP) [kg CO2-eq]. For both of these metrics, we have parameterized dynamic characterization functions for all GHG's that IPCC AR6 provides data for.
For the dynamic characterization, users can also choose the length of the considered time horizon (time_horizon
) and whether it is a fixed time horizon (fixed_time_horizon
). Fixed means that the time horizon for all emissions (no matter when they occur) starts counting at the time of the functional unit, resulting in shorter time horizons for emissions occuring later. If the time horizon is not fixed (this is what conventional impact assessment factors assume), it starts counting from the timing of the emission.
Let's characterize our dynamic inventory, regarding radiative forcing with a fixed time horizon and the default time horizon length of 100 years:
tlca.dynamic_lcia(metric="radiative_forcing", fixed_time_horizon=True)
/Users/ajakobs/Documents/prospective_dynamic_lca/bw_timex/bw_timex/dynamic_characterization.py:71: UserWarning: No custom dynamic characterization functions provided. Using default dynamic characterization functions based on IPCC AR6 meant to work with biosphere3 flows. The flows that are characterized are based on the selection of the initially chosen impact category: ('EF v3.1', 'climate change', 'global warming potential (GWP100)'). You can look up the mapping in the bw_timex.dynamic_characterizer.characterization_function_dict. warnings.warn(
date | amount | flow | flow_name | activity | activity_name | amount_sum | |
---|---|---|---|---|---|---|---|
0 | 2022-01-01 05:49:12 | 4.281176e-43 | 1332 | Ethane, 1,1,1-trifluoro-, HFC-143a | 98282 | (db_2020, ba87aff6361d99be2636e8c59e55a5b2) | 4.281176e-43 |
149 | 2022-01-01 05:49:12 | 1.528897e-19 | 260 | Tetrachloroethylene | 98282 | (db_2020, ba87aff6361d99be2636e8c59e55a5b2) | 2.138694e-12 |
150 | 2022-01-01 05:49:12 | -6.357518e-19 | 3636 | Carbon dioxide, to soil or biomass stock | 98281 | (db_2020, 133b33cc867081af144475d62179286b) | 2.138693e-12 |
151 | 2022-01-01 05:49:12 | 2.461282e-22 | 1656 | Methane, monochloro-, R-40 | 98283 | (db_2020, f6d3f0b01e4a38c055e3c5c1356a4bba) | 2.138693e-12 |
152 | 2022-01-01 05:49:12 | 1.754148e-19 | 1170 | Carbon dioxide, fossil | 98281 | (db_2020, 133b33cc867081af144475d62179286b) | 2.138693e-12 |
... | ... | ... | ... | ... | ... | ... | ... |
140225 | 2123-01-01 21:14:24 | 1.005114e-17 | 444 | Methane, trifluoro-, HFC-23 | 98308 | (db_2020, 82ebcdf42e8512cbe00151dda6210d29) | 1.351929e-09 |
140224 | 2123-01-01 21:14:24 | 3.203941e-21 | 4261 | Ethane, 1,1,2-trichloro-1,2,2-trifluoro-, CFC-113 | 98309 | (db_2020, f8114e0ff375b3c6d72ccfa49f79e44d) | 1.351929e-09 |
140223 | 2123-01-01 21:14:24 | 3.410371e-21 | 4686 | Dinitrogen monoxide | 98309 | (db_2020, f8114e0ff375b3c6d72ccfa49f79e44d) | 1.351929e-09 |
140246 | 2123-01-01 21:14:24 | 5.082378e-18 | 438 | Methane, fossil | 98308 | (db_2020, 82ebcdf42e8512cbe00151dda6210d29) | 1.351929e-09 |
140295 | 2123-01-01 21:14:24 | 2.810623e-47 | 1332 | Ethane, 1,1,1-trifluoro-, HFC-143a | 98309 | (db_2020, f8114e0ff375b3c6d72ccfa49f79e44d) | 1.351996e-09 |
140296 rows × 7 columns
The method call returns a dataframe of all the individual emissions at their respective timesteps, but we can also just look at the overall score:
# inventory from timeline
tlca.dynamic_score #W/m2 (radiative forcing)
1.3519955746298651e-09
tlca.dynamic_score #W/m2 (radiative forcing)
1.351995574629866e-09
To visualize the results, we provide a simple plotting functions:
tlca.plot_dynamic_characterized_inventory(sum_emissions_within_activity=True)
tlca.plot_dynamic_characterized_inventory(sum_emissions_within_activity=True)
Without summing up the emissions within the activity, one can see that there are also negative emissions in the system, which stem from the premise-induced BECCS in the future electricity production:
tlca.plot_dynamic_characterized_inventory()
There is also a flag to plot the cumulative radiative forcing:
tlca.plot_dynamic_characterized_inventory(sum_activities= True, cumsum=True)
Similar options are available for the metric GWP, which compares the radiative forcing of a GHG to that of CO2 over a certain time horizon (commonly 100 years, but it can be set flexibly in time_horizon
).
tlca.dynamic_lcia(metric="GWP", fixed_time_horizon=False, time_horizon = 70)
tlca.dynamic_score #kg CO2-eq (GWP)
/Users/timodiepers/Documents/Coding/timex/bw_timex/dynamic_characterization.py:70: UserWarning: No custom dynamic characterization functions provided. Using default dynamic characterization functions based on IPCC AR6 meant to work with biosphere3 flows. The flows that are characterized are based on the selection of the initially chosen impact category: ('EF v3.1', 'climate change', 'global warming potential (GWP100)'). You can look up the mapping in the bw_timex.dynamic_characterizer.characterization_function_dict. warnings.warn( /Users/timodiepers/Documents/Coding/timex/bw_timex/dynamic_characterization.py:122: UserWarning: Using bw_timex's default CO2 characterization function for GWP reference. warnings.warn(
15896.845450102795
Plotting the GWP results over time:
tlca.plot_dynamic_characterized_inventory()
Cumulative:
tlca.plot_dynamic_characterized_inventory(sum_emissions_within_activity=True, cumsum=True)
It's helpful to understand how the time-explicit results differ from those using static assessments.
We compare the time-explicit results with those of an LCA for the year 2020 and 2040 for the standard GWP100 metric (time horizon=100 and no fixed time horizon). This means we neglect the additional differences of the time-explicit results that would arise from using dynamic LCIA.
Time-explicit scores:
tlca.dynamic_lcia(metric="GWP", fixed_time_horizon=False, time_horizon=100)
tlca.dynamic_score
/Users/timodiepers/Documents/Coding/timex/bw_timex/dynamic_characterization.py:70: UserWarning: No custom dynamic characterization functions provided. Using default dynamic characterization functions based on IPCC AR6 meant to work with biosphere3 flows. The flows that are characterized are based on the selection of the initially chosen impact category: ('EF v3.1', 'climate change', 'global warming potential (GWP100)'). You can look up the mapping in the bw_timex.dynamic_characterizer.characterization_function_dict. warnings.warn( /Users/timodiepers/Documents/Coding/timex/bw_timex/dynamic_characterization.py:122: UserWarning: Using bw_timex's default CO2 characterization function for GWP reference. warnings.warn(
15446.54411830293
The 2020 (static) score has already been calculated by TimexLCA in the beginning, originally to set the priorities for the graph traversal. But we can still access the score:
tlca.base_lca.score
31401.500705260198
However, further down we also want to look at what part of the life cycle has what contribution. To get this info, we need some more calculations:
import bw2calc as bc
static_scores = {}
for e in ev_lifecycle.exchanges():
if e.input == ev_lifecycle.key:
continue
lca = bc.LCA({e.input: e.amount}, method)
lca.lci() # one could probably do this more efficiently by using .redo_lcia, but who doesn't like a 15s break :)
lca.lcia()
static_scores[e.input["name"]] = lca.score
Similarly, we calculate the 2040 (prospective) scores by just changing the database the exchanges point to:
#first create a copy of the system and relink to processes from 2040 database
prospective_ev_lifecycle = ev_lifecycle.copy()
for exc in prospective_ev_lifecycle.exchanges():
if exc.input == prospective_ev_lifecycle:
continue
exc.input = bd.get_node(
**{
"database": "db_2040",
"name": exc.input["name"],
"product": exc.input["reference product"],
"location": exc.input["location"],
}
)
exc.save()
prospective_scores = {}
for e in prospective_ev_lifecycle.exchanges():
if e.input == prospective_ev_lifecycle.key:
continue
lca = bc.LCA({e.input: e.amount}, method)
lca.lci()
lca.lcia()
prospective_scores[e.input["name"]] = lca.score
Lets compare the overall scores:
print("Static score: ", sum(static_scores.values())) # should be the same as tlca.base_lca.score
print("Prospective score: ", sum(prospective_scores.values()))
print("Time-explicit score: ", tlca.dynamic_score)
Static score: 31401.500710765587 Prospective score: 7234.099282744538 Time-explicit score: 15446.54411830293
To better understand what's going on, let's plot the scores as a waterfall chart based on timing of emission. Also, we can look at the "first-level contributions":
from bw_timex.utils import plot_characterized_inventory_as_waterfall
order_stacked_activities = (
[ # to sort stacked bars in waterfall plot chronologically from production to EoL
"market for glider, passenger car",
"market for powertrain, for electric passenger car",
"battery production, Li-ion, LiMn2O4, rechargeable, prismatic",
"market group for electricity, low voltage",
"market for manual dismantling of used electric passenger car",
"market for used Li-ion battery",
]
)
plot_characterized_inventory_as_waterfall(
tlca.characterized_inventory,
metric=tlca.metric,
static_scores=static_scores,
prospective_scores=prospective_scores,
order_stacked_activities=order_stacked_activities,
)
One can see that the time-explicit results (in the middle) are somewhere in between the static and the prospective results. This makes sense as at each timestep, the underlying processes are sourced from progressively "cleaner" background databases, reaching a lower impact than if they are only sourced from the current database, but not so low as the prospective results, which are fully sourced from the most decarbonized database. Notably, the electricity consumption in the use-phase, modelled uniformly over the lifetime of the EV, contributes less and less to the score in the later years, since the electricity becomes cleaner in the future databases.