From 740cb0ed8ab1b80d2cee5abc70eb2c8eed138558 Mon Sep 17 00:00:00 2001 From: TimoDiepers Date: Thu, 19 Sep 2024 08:31:41 +0200 Subject: [PATCH 1/2] rounding dates instead of cutting off --- bw_timex/dynamic_biosphere_builder.py | 2 +- bw_timex/timeline_builder.py | 12 +++++-- bw_timex/timex_lca.py | 26 +++----------- bw_timex/utils.py | 49 ++++++++++++++++++++------- 4 files changed, 53 insertions(+), 36 deletions(-) diff --git a/bw_timex/dynamic_biosphere_builder.py b/bw_timex/dynamic_biosphere_builder.py index fe14d13..4aeb2cf 100644 --- a/bw_timex/dynamic_biosphere_builder.py +++ b/bw_timex/dynamic_biosphere_builder.py @@ -235,7 +235,7 @@ def build_dynamic_biosphere_matrix( td_producer = TemporalDistribution( date=np.array([str(time_in_datetime)], dtype=self.time_res), amount=np.array([1]), - ).date # TODO: Simplify + ).date date = td_producer[0] time_mapped_matrix_id = self.biosphere_time_mapping_dict.add( diff --git a/bw_timex/timeline_builder.py b/bw_timex/timeline_builder.py index 973f0ae..9289c12 100644 --- a/bw_timex/timeline_builder.py +++ b/bw_timex/timeline_builder.py @@ -13,6 +13,7 @@ convert_date_string_to_datetime, extract_date_as_integer, extract_date_as_string, + round_datetime, ) @@ -157,11 +158,18 @@ def build_timeline(self) -> pd.DataFrame: edges_df["consumer"] == -1, "producer_date" ] + edges_df["rounded_consumer_date"] = edges_df["consumer_date"].apply( + lambda x: round_datetime(x, self.temporal_grouping) + ) + edges_df["rounded_producer_date"] = edges_df["producer_date"].apply( + lambda x: round_datetime(x, self.temporal_grouping) + ) + # extract grouping time of consumer and producer: processes occuring at different times within in the same time window of grouping get the same grouping time - edges_df["consumer_grouping_time"] = edges_df["consumer_date"].apply( + edges_df["consumer_grouping_time"] = edges_df["rounded_consumer_date"].apply( lambda x: extract_date_as_string(x, self.temporal_grouping) ) - edges_df["producer_grouping_time"] = edges_df["producer_date"].apply( + edges_df["producer_grouping_time"] = edges_df["rounded_producer_date"].apply( lambda x: extract_date_as_string(x, self.temporal_grouping) ) diff --git a/bw_timex/timex_lca.py b/bw_timex/timex_lca.py index 2f23d9f..b2b7361 100644 --- a/bw_timex/timex_lca.py +++ b/bw_timex/timex_lca.py @@ -31,19 +31,15 @@ from .helper_classes import SetList, TimeMappingDict from .matrix_modifier import MatrixModifier from .timeline_builder import TimelineBuilder -from .utils import ( - extract_date_as_integer, - resolve_temporalized_node_name, - round_datetime_to_nearest_year, -) +from .utils import extract_date_as_integer, resolve_temporalized_node_name class TimexLCA: """ Class to perform time-explicit LCA calculations. - A TimexLCA contains the LCI of processes occuring at explicit points in time. It tracks the timing of processes, - relinks their technosphere and biosphere exchanges to match the technology landscape at that point in time, + A TimexLCA contains the LCI of processes occuring at explicit points in time. It tracks the timing of processes, + relinks their technosphere and biosphere exchanges to match the technology landscape at that point in time, and also keeps track of the timing of the resulting emissions. As such, it combines prospective and dynamic LCA approaches. @@ -255,7 +251,7 @@ def build_timeline( ) self.timeline = self.timeline_builder.build_timeline() - + return self.timeline[ [ "date_producer", @@ -447,18 +443,6 @@ def dynamic_lcia( # Set a default for inventory_in_time_horizon using the full dynamic_inventory_df inventory_in_time_horizon = self.dynamic_inventory_df - # Round dates to nearest year and sum up emissions for each year - inventory_in_time_horizon.date = inventory_in_time_horizon.date.apply( - round_datetime_to_nearest_year - ) - inventory_in_time_horizon = ( - inventory_in_time_horizon.groupby( - inventory_in_time_horizon.columns.tolist() - ) - .sum() - .reset_index() - ) - # 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) @@ -1103,7 +1087,7 @@ def add_static_activities_to_time_mapping_dict(self) -> None: (('database', 'code'), datetime_as_integer): time_mapping_id) that is later used to uniquely identify time-resolved processes. Here, the activity_time_mapping_dict is the pre-population with the static activities. The time-explicit activities (from other temporalized background - databases) are added later on by the TimelineBuilder. Activities in the foreground database are + databases) are added later on by the TimelineBuilder. Activities in the foreground database are mapped with (('database', 'code'), "dynamic"): time_mapping_id)" as their timing is not yet known. Parameters diff --git a/bw_timex/utils.py b/bw_timex/utils.py index 37612f5..7025e16 100644 --- a/bw_timex/utils.py +++ b/bw_timex/utils.py @@ -1,5 +1,5 @@ import warnings -from datetime import datetime +from datetime import datetime, timedelta from typing import Callable, List, Optional, Union import bw2data as bd @@ -115,25 +115,49 @@ def convert_date_string_to_datetime(temporal_grouping, datestring) -> datetime: return datetime.strptime(datestring, time_res_dict[temporal_grouping]) -def round_datetime_to_nearest_year(date: datetime) -> datetime: +def round_datetime(date: datetime, resolution: str) -> datetime: """ - Round a datetime object to the nearest year. + Round a datetime object based on a given resolution + + Parameters + ---------- + date : datetime + datetime object to be rounded + resolution: str + Temporal resolution to round the datetime object to. Options are: 'year', 'month', 'day' and + 'hour'. Returns ------- datetime - datetime object rounded to nearest year. + rounded datetime object """ - year = date.year - start_of_year = pd.Timestamp(f"{year}-01-01") - start_of_next_year = pd.Timestamp(f"{year+1}-01-01") + if resolution == "year": + mid_year = pd.Timestamp(f"{date.year}-07-01") + return ( + pd.Timestamp(f"{date.year+1}-01-01") + if date >= mid_year + else pd.Timestamp(f"{date.year}-01-01") + ) + + elif resolution == "month": + start_of_month = pd.Timestamp(f"{date.year}-{date.month}-01") + next_month = start_of_month + pd.DateOffset(months=1) + mid_month = start_of_month + (next_month - start_of_month) / 2 + return next_month if date >= mid_month else start_of_month + + elif resolution == "day": + start_of_day = datetime(date.year, date.month, date.day) + mid_day = start_of_day + timedelta(hours=12) + return start_of_day + timedelta(days=1) if date >= mid_day else start_of_day - mid_year = start_of_year + (start_of_next_year - start_of_year) / 2 + elif resolution == "hour": + start_of_hour = datetime(date.year, date.month, date.day, date.hour) + mid_hour = start_of_hour + timedelta(minutes=30) + return start_of_hour + timedelta(hours=1) if date >= mid_hour else start_of_hour - if date < mid_year: - return start_of_year else: - return start_of_next_year + raise ValueError("Resolution must be one of 'year', 'month', 'day', or 'hour'.") def add_flows_to_characterization_function_dict( @@ -151,7 +175,8 @@ def add_flows_to_characterization_function_dict( 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. + Dictionary of flows and their corresponding characterization functions. Default is an empty + dictionary. Returns ------- From e4107c8264fda05dcf772df7bd36bcff54091d31 Mon Sep 17 00:00:00 2001 From: TimoDiepers Date: Thu, 19 Sep 2024 08:32:10 +0200 Subject: [PATCH 2/2] linting --- bw_timex/utils.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/bw_timex/utils.py b/bw_timex/utils.py index 7025e16..2356374 100644 --- a/bw_timex/utils.py +++ b/bw_timex/utils.py @@ -2,7 +2,6 @@ from datetime import datetime, timedelta from typing import Callable, List, Optional, Union -import bw2data as bd import matplotlib.pyplot as plt import pandas as pd from bw2data.backends import ActivityDataset as AD @@ -140,24 +139,23 @@ def round_datetime(date: datetime, resolution: str) -> datetime: else pd.Timestamp(f"{date.year}-01-01") ) - elif resolution == "month": + if resolution == "month": start_of_month = pd.Timestamp(f"{date.year}-{date.month}-01") next_month = start_of_month + pd.DateOffset(months=1) mid_month = start_of_month + (next_month - start_of_month) / 2 return next_month if date >= mid_month else start_of_month - elif resolution == "day": + if resolution == "day": start_of_day = datetime(date.year, date.month, date.day) mid_day = start_of_day + timedelta(hours=12) return start_of_day + timedelta(days=1) if date >= mid_day else start_of_day - elif resolution == "hour": + if resolution == "hour": start_of_hour = datetime(date.year, date.month, date.day, date.hour) mid_hour = start_of_hour + timedelta(minutes=30) return start_of_hour + timedelta(hours=1) if date >= mid_hour else start_of_hour - else: - raise ValueError("Resolution must be one of 'year', 'month', 'day', or 'hour'.") + raise ValueError("Resolution must be one of 'year', 'month', 'day', or 'hour'.") def add_flows_to_characterization_function_dict(