Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rounding dates instead of cutting off #95

Merged
merged 2 commits into from
Sep 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion bw_timex/dynamic_biosphere_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
12 changes: 10 additions & 2 deletions bw_timex/timeline_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
convert_date_string_to_datetime,
extract_date_as_integer,
extract_date_as_string,
round_datetime,
)


Expand Down Expand Up @@ -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)
)

Expand Down
26 changes: 5 additions & 21 deletions bw_timex/timex_lca.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -255,7 +251,7 @@ def build_timeline(
)

self.timeline = self.timeline_builder.build_timeline()

return self.timeline[
[
"date_producer",
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
51 changes: 37 additions & 14 deletions bw_timex/utils.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import warnings
from datetime import datetime
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
Expand Down Expand Up @@ -115,25 +114,48 @@ 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")
)

mid_year = start_of_year + (start_of_next_year - start_of_year) / 2
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

if date < mid_year:
return start_of_year
else:
return start_of_next_year
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

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

raise ValueError("Resolution must be one of 'year', 'month', 'day', or 'hour'.")


def add_flows_to_characterization_function_dict(
Expand All @@ -151,7 +173,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
-------
Expand Down