Skip to content

Commit

Permalink
Merge pull request #72 from brightway-lca/refactored_dynamic_characte…
Browse files Browse the repository at this point in the history
…rization

Refactored dynamic characterization
  • Loading branch information
TimoDiepers authored Jul 17, 2024
2 parents 78c68fe + 89af837 commit f01c5b0
Show file tree
Hide file tree
Showing 11 changed files with 217 additions and 973 deletions.
5 changes: 4 additions & 1 deletion CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
## v0.1.8 (2024-07-17)
* Moved dynamic characterization functionality completely to [dynamic_characterization](https://github.com/brightway-lca/dynamic_characterization). In the course of this, it was dynamic characterization was updated and is much faster now. See also https://github.com/brightway-lca/dynamic_characterization/pull/3

## v0.1.7 (2024-07-11)
* Fixed some dependencies

Expand All @@ -6,7 +9,7 @@
* Added option to calculate the dynamic LCI directly from the timeline without expanding the technosphere matrix

## v0.1.5 (2024-06-28)
* Refactored dynamic characterization to separate package [dynamic_characterization](https://github.com/TimoDiepers/dynamic_characterization)
* Refactored dynamic characterization to separate package [dynamic_characterization](https://github.com/brightway-lca/dynamic_characterization)

## v0.1.4 (2024-06-15)
* Handled emissions occuring outside of fixed time horizon in dynamic characterization [#46](https://github.com/brightway-lca/bw_timex/issues/46)
Expand Down
3 changes: 1 addition & 2 deletions bw_timex/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
from .dynamic_biosphere_builder import DynamicBiosphereBuilder
from .dynamic_characterization import DynamicCharacterization
from .edge_extractor import EdgeExtractor
from .helper_classes import SetList
from .matrix_modifier import MatrixModifier
from .timeline_builder import TimelineBuilder
from .timex_lca import TimexLCA

__version__ = "0.1.7"
__version__ = "0.1.8"
398 changes: 0 additions & 398 deletions bw_timex/dynamic_characterization.py

This file was deleted.

120 changes: 57 additions & 63 deletions bw_timex/timex_lca.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,12 @@
from bw2data.backends.schema import ActivityDataset as AD
from bw2data.backends.schema import get_id
from bw2data.errors import Brightway2Project
from dynamic_characterization import characterize_dynamic_inventory
from peewee import fn
from scipy import sparse

# from .dynamic_biosphere_builder import DynamicBiosphereBuilder
from .dynamic_biosphere_builder import DynamicBiosphereBuilder
from .dynamic_characterization import DynamicCharacterization
from .helper_classes import SetList, TimeMappingDict
from .matrix_modifier import MatrixModifier
from .timeline_builder import TimelineBuilder
Expand Down Expand Up @@ -193,9 +193,13 @@ def build_timeline(
self.cutoff = cutoff
self.max_calc = max_calc

self.add_static_activities_to_time_mapping_dict() # pre-populate the activity time mapping dict with the static activities. Doing this here because we need the temporal grouping for consistent times resolution.
# pre-populate the activity time mapping dict with the static activities.
# Doing this here because we need the temporal grouping for consistent times resolution.
self.add_static_activities_to_time_mapping_dict()

# Create timeline builder that does the graph traversal (similar to bw_temporalis) and extracts all edges with their temporal information. Can later be used to build a timeline with the TimelineBuilder.build_timeline() method.
# Create timeline builder that does the graph traversal (similar to bw_temporalis) and extracts
# all edges with their temporal information. Can later be used to build a timeline with the
# TimelineBuilder.build_timeline() method.
self.timeline_builder = TimelineBuilder(
self.static_lca,
self.edge_filter_function,
Expand Down Expand Up @@ -322,11 +326,12 @@ def static_lcia(self) -> None:

def dynamic_lcia(
self,
metric: str | None = "GWP",
time_horizon: int | None = 100,
fixed_time_horizon: bool | None = False,
metric: str = "radiative_forcing",
time_horizon: int = 100,
fixed_time_horizon: bool = False,
time_horizon_start: datetime = None,
characterization_function_dict: dict = None,
cumsum: bool | None = True,
characterization_function_co2: dict = None,
) -> pd.DataFrame:
"""
Calculates dynamic LCIA with the `DynamicCharacterization` class using the dynamic inventory and dynamic
Expand All @@ -347,15 +352,18 @@ def dynamic_lcia(
Parameters
----------
metric : str, optional
the metric for which the dynamic LCIA should be calculated. Default is "GWP". Available: "GWP" and "radiative_forcing"
the metric for which the dynamic LCIA should be calculated. Default is "radiative_forcing". Available: "GWP" and "radiative_forcing"
time_horizon: int, optional
the time horizon for the impact assessment. Unit is years. Default is 100.
fixed_time_horizon: bool, optional
Whether the emission time horizon for all emissions is calculated from the functional unit (fixed_time_horizon=True) or from the time of the emission (fixed_time_horizon=False). Default is False.
time_horizon_start: pd.Timestamp, optional
The starting timestamp of the time horizon for the dynamic characterization. Only needed for fixed time horizons. Default is datetime.now().
characterization_function_dict: dict, optional
Dict of the form {biosphere_flow_database_id: characterization_function}. Default is None, which triggers the use of the provided dynamic characterization functions based on IPCC AR6 Chapter 7.
cumsum: bool, optional
Whether to calculate the cumulative sum of the characterization results. Default is True.
characterization_function_co2: Callable, optional
Characterization function for CO2 emissions. Necessary if GWP metrix is chosen. Default is None, which triggers the use of the provided dynamic characterization function of co2 based on IPCC AR6 Chapter 7.
Returns
-------
Expand All @@ -372,36 +380,36 @@ def dynamic_lcia(
"Dynamic lci not yet calculated. Call TimexLCA.calculate_dynamic_lci() first."
)

# Set a default for inventory_in_time_horizon using the full dynamic_inventory_df
inventory_in_time_horizon = self.dynamic_inventory_df

# Calculate the latest considered impact date
t0_date = pd.Timestamp(self.timeline_builder.edge_extractor.t0.date[0])
latest_considered_impact = t0_date + pd.DateOffset(years=time_horizon)
last_emission = self.dynamic_inventory_df.date.max()
if fixed_time_horizon and latest_considered_impact < last_emission:
warnings.warn(
"An emission occurs outside of the specified time horizon and will not be characterized. Please make sure this is intended."
)
self.dynamic_inventory_df = self.dynamic_inventory_df[
self.dynamic_inventory_df.date <= latest_considered_impact
]

self.metric = metric
self.time_horizon = time_horizon
self.fixed_time_horizon = fixed_time_horizon

self.dynamic_characterizer = DynamicCharacterization(
self.dynamic_inventory_df,
self.demand_timing_dict,
self.temporal_grouping,
self.method,
characterization_function_dict,
)
# Update inventory_in_time_horizon if a fixed time horizon is used
if fixed_time_horizon:
last_emission = self.dynamic_inventory_df.date.max()
if latest_considered_impact < last_emission:
warnings.warn(
"An emission occurs outside of the specified time horizon and will not be characterized. Please make sure this is intended."
)
inventory_in_time_horizon = self.dynamic_inventory_df[
self.dynamic_inventory_df.date <= latest_considered_impact
]

self.characterized_inventory = (
self.dynamic_characterizer.characterize_dynamic_inventory(
metric,
time_horizon,
fixed_time_horizon,
cumsum,
)
if not time_horizon_start:
time_horizon_start = t0_date

self.characterized_inventory = characterize_dynamic_inventory(
dynamic_inventory_df=inventory_in_time_horizon,
metric=metric,
characterization_function_dict=characterization_function_dict,
base_lcia_method=self.method,
time_horizon=time_horizon,
fixed_time_horizon=fixed_time_horizon,
time_horizon_start=time_horizon_start,
characterization_function_co2=characterization_function_co2,
)

self.dynamic_score = self.characterized_inventory["amount"].sum()
Expand Down Expand Up @@ -1153,7 +1161,7 @@ def plot_dynamic_characterized_inventory(

if not hasattr(self, "characterized_inventory"):
warnings.warn(
"Characterized inventory not yet calculated. Call TimexLCA.characterize_dynamic_lci() first."
"Characterized inventory not yet calculated. Call TimexLCA.dynamic_lcia() first."
)
return

Expand All @@ -1175,48 +1183,34 @@ def plot_dynamic_characterized_inventory(
plot_data["amount_sum"] = plot_data["amount"].cumsum()
plot_data["activity_label"] = "All activities"

else:
plot_data["activity_label"] = plot_data.apply(
lambda row: bd.get_activity(
self.activity_time_mapping_dict_reversed[row.activity][0]
)["name"],
axis=1,
)
# Plotting
plt.figure(figsize=(14, 6))
axes = sb.scatterplot(x="date", y=amount, hue="activity_label", data=plot_data)
else: # plotting activities separate

# Determine the plotting labels and titles based on the characterization method
if self.metric == "radiative_forcing":
label_legend = "Radiative forcing [W/m2]"
title = "Radiative forcing"
elif self.metric == "GWP":
label_legend = "GWP [kg CO2-eq]"
title = "GWP"
activity_name_cache = {}

if self.fixed_time_horizon:
suptitle = f" \nTH of {self.time_horizon} years starting at FU,"
else:
suptitle = f" \nTH of {self.time_horizon} years starting at each emission,"
for activity in plot_data["activity"].unique():
if activity not in activity_name_cache:
activity_name_cache[activity] = bd.get_activity(
self.activity_time_mapping_dict_reversed[activity][0]
)["name"]

plot_data["activity_label"] = plot_data["activity"].map(activity_name_cache)

suptitle += f" temporal resolution of inventories: {self.temporal_grouping}"
# Plotting
plt.figure(figsize=(14, 6))
axes = sb.scatterplot(x="date", y=amount, hue="activity_label", data=plot_data)

# Determine y-axis limit flexibly
if plot_data[amount].min() < 0:
ymin = plot_data[amount].min() * 1.1
else:
ymin = 0

axes.set_title(title)
axes.set_axisbelow(True)
axes.set_ylim(bottom=ymin)
axes.set_ylabel(label_legend)
axes.set_xlabel("Time")

handles, labels = axes.get_legend_handles_labels()
axes.legend(handles[::-1], labels[::-1])
plt.title(title, fontsize=16, y=1.05)
plt.suptitle(suptitle, fontsize=12, y=0.95)
plt.grid()
plt.show()

Expand Down
54 changes: 24 additions & 30 deletions bw_timex/utils.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import warnings
from datetime import datetime
from typing import Callable, List, Optional, Union

import bw2data as bd
import matplotlib.pyplot as plt
import pandas as pd
import bw2data as bd

from datetime import datetime
from typing import Callable, Union, List, Optional


def extract_date_as_integer(dt_obj: datetime, time_res: Optional[str] = "year") -> int:
Expand Down Expand Up @@ -58,7 +58,7 @@ def extract_date_as_string(temporal_grouping: str, timestamp: datetime) -> str:
Temporal grouping for the date string. Options are: 'year', 'month', 'day', 'hour'
timestamp : datetime
Datetime object to be converted to a string.
Returns
-------
date_as_string
Expand Down Expand Up @@ -92,7 +92,7 @@ def convert_date_string_to_datetime(temporal_grouping, datestring) -> datetime:
Temporal grouping for the date string. Options are: 'year', 'month', 'day', 'hour'
datestring : str
Date as a string
Returns
-------
datetime
Expand Down Expand Up @@ -126,12 +126,12 @@ def add_flows_to_characterization_function_dict(
Parameters
----------
flows : Union[str, List[str]]
Flow or list of flows to be added to the characterization function dictionary.
Flow or list of flows to be added to the characterization function dictionary.
func : Callable
Dynamic characterization function for flow.
characterization_function_dict : dict, optional
Dictionary of flows and their corresponding characterization functions. Default is an empty dictionary.
Returns
-------
dict
Expand All @@ -151,8 +151,7 @@ def add_flows_to_characterization_function_dict(


def plot_characterized_inventory_as_waterfall(
characterized_inventory,
metric,
lca_obj,
static_scores=None,
prospective_scores=None,
order_stacked_activities=None,
Expand All @@ -163,10 +162,8 @@ def plot_characterized_inventory_as_waterfall(
Parameters
----------
characterized_inventory : pd.DataFrame
Dataframe of dynamic characterized inventory, such as TimexLCA.characterized_inventory
metric : str
Impact assessment method. Only 'GWP' is supported at the moment.
lca_obj : TimexLCA
LCA object with characterized inventory data.
static_scores : dict, optional
Dictionary of static scores. Default is None.
prospective_scores : dict, optional
Expand All @@ -179,32 +176,29 @@ def plot_characterized_inventory_as_waterfall(
None but plots the waterfall chart.
"""
# Check for necessary columns
if not {"date", "activity_name", "amount"}.issubset(
characterized_inventory.columns
):
raise ValueError(
"DataFrame must contain 'date', 'activity_name', and 'amount' columns."
)
if metric != "GWP":
raise NotImplementedError(
f"Only GWP metric is supported at the moment, not {metric}."
)
if not hasattr(lca_obj, "characterized_inventory"):
raise ValueError("LCA object does not have characterized inventory data.")

if not hasattr(lca_obj, "activity_time_mapping_dict_reversed"):
raise ValueError("Make sure to pass an instance of a TimexLCA.")

# Grouping and summing data
plot_data = characterized_inventory.groupby(
["date", "activity_name"], as_index=False
plot_data = lca_obj.characterized_inventory.groupby(
["date", "activity"], as_index=False
).sum()
plot_data["year"] = plot_data[
"date"
].dt.year # TODO make temporal resolution flexible

# Optimized activity label fetching
unique_activities = plot_data["activity_name"].unique()
unique_activities = plot_data["activity"].unique()
activity_labels = {
activity: bd.get_activity(activity)["name"] for activity in unique_activities
idx: bd.get_activity(lca_obj.activity_time_mapping_dict_reversed[idx][0])[
"name"
]
for idx in unique_activities
}
plot_data["activity_label"] = plot_data["activity_name"].map(activity_labels)
plot_data["activity_label"] = plot_data["activity"].map(activity_labels)
# Pivoting data for plotting
pivoted_data = plot_data.pivot(
index="year", columns="activity_label", values="amount"
Expand Down
3 changes: 1 addition & 2 deletions notebooks/example_setac.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -1683,8 +1683,7 @@
")\n",
"\n",
"plot_characterized_inventory_as_waterfall(\n",
" tlca.characterized_inventory,\n",
" metric=tlca.metric,\n",
" tlca,\n",
" static_scores=static_scores,\n",
" prospective_scores=prospective_scores,\n",
" order_stacked_activities=order_stacked_activities,\n",
Expand Down
Loading

0 comments on commit f01c5b0

Please sign in to comment.