Skip to content

Commit

Permalink
Merge pull request #89 from brightway-lca/clarify_sub_lcas
Browse files Browse the repository at this point in the history
Clarify naming of internal sub-LCAs and scores.
  • Loading branch information
muelleram authored Sep 11, 2024
2 parents 658874e + 123e5fa commit eb901b5
Show file tree
Hide file tree
Showing 12 changed files with 80 additions and 65 deletions.
2 changes: 1 addition & 1 deletion bw_timex/timeline_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ def __init__(
temporal_grouping: str = "year",
interpolation_type: str = "linear",
cutoff: float = 1e-9,
max_calc: float = 1e4,
max_calc: int = 2000,
*args,
**kwargs,
) -> None:
Expand Down
104 changes: 59 additions & 45 deletions bw_timex/timex_lca.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ class TimexLCA:
TimexLCA calculates:
1) a static LCA score (`TimexLCA.static_lca.score`, same as `bw2calc.lca.score`),
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.
Expand Down Expand Up @@ -124,12 +124,12 @@ def __init__(
self.create_node_id_collection_dict()

# Calculate static LCA results using a custom prepare_lca_inputs function that includes all background databases in the LCA. We need all the IDs for the time mapping dict.
fu, data_objs, remapping = self.prepare_static_lca_inputs(
fu, data_objs, remapping = self.prepare_base_lca_inputs(
demand=self.demand, method=self.method
)
self.static_lca = LCA(fu, data_objs=data_objs, remapping_dicts=remapping)
self.static_lca.lci()
self.static_lca.lcia()
self.base_lca = LCA(fu, data_objs=data_objs, remapping_dicts=remapping)
self.base_lca.lci()
self.base_lca.lcia()

# Create a time mapping dict that maps each activity to a activity_time_mapping_id in the format (('database', 'code'), datetime_as_integer): time_mapping_id)
self.activity_time_mapping_dict = TimeMappingDict(
Expand All @@ -149,7 +149,7 @@ def build_timeline(
interpolation_type: str = "linear",
edge_filter_function: Callable = None,
cutoff: float = 1e-9,
max_calc: float = 1e4,
max_calc: int = 2000,
*args,
**kwargs,
) -> pd.DataFrame:
Expand Down Expand Up @@ -215,7 +215,7 @@ def build_timeline(
# 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.base_lca,
self.edge_filter_function,
self.database_date_dict,
self.database_date_dict_static_only,
Expand Down Expand Up @@ -336,12 +336,12 @@ def static_lcia(self) -> None:
"""
if not hasattr(self, "lca"):
raise AttributeError("LCI not yet calculated. Call TimexLCA.lci() first.")
return
if not self.expanded_technosphere:
raise ValueError("Currently the static lcia score can only be calculated if the expanded matrix has been built\
Please call TimexLCA.lci(expand_technosphere=True) first.")
raise ValueError(
"Currently the static lcia score can only be calculated if the expanded matrix has been built\
Please call TimexLCA.lci(expand_technosphere=True) first."
)
self.lca.lcia()
self.static_score = self.lca.score

def dynamic_lcia(
self,
Expand All @@ -354,13 +354,13 @@ def dynamic_lcia(
) -> pd.DataFrame:
"""
Calculates dynamic LCIA with the `DynamicCharacterization` class using the dynamic inventory and dynamic
characterization functions. Dynamic characterization is handled by the separate package
characterization functions. Dynamic characterization is handled by the separate package
`dynamic_characterization` (https://dynamic-characterization.readthedocs.io/en/latest/).
Dynamic characterization functions in the form of a dictionary {biosphere_flow_database_id:
characterization_function} can be given by the user.
If none are given, a set of default dynamic characterization functions based on IPCC AR6 are provided from
`dynamic_characterization` package. These are mapped to the biosphere3 flows of the chosen static climate
`dynamic_characterization` package. These are mapped to the biosphere3 flows of the chosen static climate
change impact category. If there is no characterization function for a biosphere flow, it will be ignored.
Two dynamic climate change metrics are provided: "GWP" and "radiative_forcing".
Expand Down Expand Up @@ -388,7 +388,7 @@ def dynamic_lcia(
Returns
-------
pandas.DataFrame
A Dataframe with the characterized inventory for the chosen metric and parameters. Also stores the sum as attribute `dynamic_score`.
A Dataframe with the characterized inventory for the chosen metric and parameters.
See also
--------
Expand Down Expand Up @@ -432,10 +432,40 @@ def dynamic_lcia(
characterization_function_co2=characterization_function_co2,
)

self.dynamic_score = self.characterized_inventory["amount"].sum()

return self.characterized_inventory

###################
# Core properties #
###################

@property
def base_score(self) -> float:
"""
Score of the base LCA, i.e., the "normal" LCA without time-explicit information.
Same as when using bw2calc.LCA.score
"""
return self.base_lca.score

@property
def static_score(self) -> float:
"""
Score resulting from the static LCIA of the time-explicit inventory.
"""
if not hasattr(self, "lca"):
raise AttributeError("LCI not yet calculated. Call TimexLCA.lci() first.")
return self.lca.score

@property
def dynamic_score(self) -> float:
"""
Score resulting from the dynamic LCIA of the time-explicit inventory.
"""
if not hasattr(self, "characterized_inventory"):
raise AttributeError(
"Characterized inventory not yet calculated. Call TimexLCA.dynamic_lcia() first."
)
return self.characterized_inventory["amount"].sum()

###############################################
# Other core functions for the inner workings #
###############################################
Expand Down Expand Up @@ -521,9 +551,9 @@ class and then multiplying it with the dynamic supply array. The dynamic invento
)

# Build the dynamic inventory
count = len(self.dynamic_supply_array)
count = len(self.dynamic_biosphere_builder.dynamic_supply_array)
diagonal_supply_array = sparse.spdiags(
[self.dynamic_supply_array], [0], count, count
[self.dynamic_biosphere_builder.dynamic_supply_array], [0], count, count
) # diagnolization of supply array keeps the dimension of the process, which we want to pass as additional information to the dynamic inventory dict
self.dynamic_inventory = self.dynamic_biomatrix @ diagonal_supply_array

Expand All @@ -534,7 +564,9 @@ class and then multiplying it with the dynamic supply array. The dynamic invento
self.activity_time_mapping_dict_reversed = {
v: k for k, v in self.activity_time_mapping_dict.items()
}
self.dynamic_inventory_df = self.create_dynamic_inventory_dataframe(from_timeline)
self.dynamic_inventory_df = self.create_dynamic_inventory_dataframe(
from_timeline
)

def create_dynamic_inventory_dataframe(self, from_timeline=False) -> pd.DataFrame:
"""Brings the dynamic inventory from its matrix form in `dynamic_inventory` into the the format
Expand Down Expand Up @@ -574,11 +606,13 @@ def create_dynamic_inventory_dataframe(self, from_timeline=False) -> pd.DataFram
row = i
col = self.dynamic_inventory.indices[j]
value = self.dynamic_inventory.data[j]

if from_timeline:
emitting_process_id = self.timeline.iloc[col]['time_mapped_producer']
emitting_process_id = self.timeline.iloc[col][
"time_mapped_producer"
]
else:
emitting_process_id = self.activity_dict.reversed[col]
emitting_process_id = self.lca.activity_dict.reversed[col]

bioflow_id, date = self.biosphere_time_mapping_dict_reversed[
row
Expand All @@ -603,7 +637,7 @@ def create_dynamic_inventory_dataframe(self, from_timeline=False) -> pd.DataFram
# For setup #
#############

def prepare_static_lca_inputs(
def prepare_base_lca_inputs(
self,
demand=None,
method=None,
Expand Down Expand Up @@ -997,10 +1031,8 @@ def add_static_activities_to_time_mapping_dict(self) -> None:
-------
None but adds the activities to the `activity_time_mapping_dict`
"""
for idx in self.static_lca.dicts.activity.keys(): # activity ids
key = self.static_lca.remapping_dicts["activity"][
idx
] # ('database', 'code')
for idx in self.base_lca.dicts.activity.keys(): # activity ids
key = self.base_lca.remapping_dicts["activity"][idx] # ('database', 'code')
time = self.database_date_dict[
key[0]
] # datetime (or 'dynamic' for foreground processes)
Expand Down Expand Up @@ -1257,21 +1289,3 @@ def remap_inventory_dicts(self) -> None:
warnings.warn(
"bw25's original mapping function doesn't work with our new time-mapped matrix entries. The Timex mapping can be found in acvitity_time_mapping_dict and biosphere_time_mapping_dict."
)

def __getattr__(self, name):
"""
Delegate attribute access to the self.lca object if the attribute
is not found in the TimexLCA instance itself, excluding special attributes.
"""
if name.startswith("__"):
raise AttributeError(
f"'{type(self).__name__}' object has no attribute '{name}'"
)
if hasattr(self.lca, name):
return getattr(self.lca, name)
if hasattr(self.dynamic_biosphere_builder, name):
return getattr(self.dynamic_biosphere_builder, name)
else:
raise AttributeError(
f"'TimexLCA' object and its 'lca'- and dynamic_biosphere_builder- attributes have no attribute '{name}'"
)
4 changes: 2 additions & 2 deletions dev/Example_long_timeline_dev_version.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -1573,7 +1573,7 @@
}
],
"source": [
"tlca.static_lca.score"
"tlca.base_lca.score"
]
},
{
Expand Down Expand Up @@ -1663,7 +1663,7 @@
}
],
"source": [
"print(\"Static score: \", sum(static_scores.values())) # should be the same as tlca.static_lca.score\n",
"print(\"Static score: \", sum(static_scores.values())) # should be the same as tlca.base_lca.score\n",
"print(\"Prospective score: \", sum(prospective_scores.values()))\n",
"print(\"Time-explicit score: \", tlca.dynamic_score)"
]
Expand Down
4 changes: 2 additions & 2 deletions dev/Example_notebook_dev_version.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -2032,7 +2032,7 @@
}
],
"source": [
"tlca.static_lca.score"
"tlca.base_lca.score"
]
},
{
Expand Down Expand Up @@ -2122,7 +2122,7 @@
}
],
"source": [
"print(\"Static score: \", sum(static_scores.values())) # should be the same as tlca.static_lca.score\n",
"print(\"Static score: \", sum(static_scores.values())) # should be the same as tlca.base_lca.score\n",
"print(\"Prospective score: \", sum(prospective_scores.values()))\n",
"print(\"Time-explicit score: \", tlca.dynamic_score)"
]
Expand Down
4 changes: 2 additions & 2 deletions dev/old_tests/old_t_bioflows.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ def filter_function(database_id: int) -> bool:
mlca.lci()
mlca.lcia()
self.assertTrue(
math.isclose(mlca.score, mlca.static_lca.score, rel_tol=1e-2),
f"Total scores didn't match up. Medusa LCA score is {mlca.score}, static LCA score is {mlca.static_lca.score}",
math.isclose(mlca.score, mlca.base_lca.score, rel_tol=1e-2),
f"Total scores didn't match up. Medusa LCA score is {mlca.score}, static LCA score is {mlca.base_lca.score}",
)

mlca.build_dynamic_biosphere()
Expand Down
4 changes: 2 additions & 2 deletions docs/content/examples/example_ev.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -1562,7 +1562,7 @@
}
],
"source": [
"tlca.static_lca.score"
"tlca.base_lca.score"
]
},
{
Expand Down Expand Up @@ -1652,7 +1652,7 @@
}
],
"source": [
"print(\"Static score: \", sum(static_scores.values())) # should be the same as tlca.static_lca.score\n",
"print(\"Static score: \", sum(static_scores.values())) # should be the same as tlca.base_lca.score\n",
"print(\"Prospective score: \", sum(prospective_scores.values()))\n",
"print(\"Time-explicit score: \", tlca.dynamic_score)"
]
Expand Down
8 changes: 4 additions & 4 deletions notebooks/example_electric_vehicle_premise.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -1536,7 +1536,7 @@
}
],
"source": [
"tlca.static_lca.score"
"tlca.base_lca.score"
]
},
{
Expand Down Expand Up @@ -1565,8 +1565,8 @@
"\n",
"static_scores = {}\n",
"for e in ev_lifecycle.technosphere():\n",
" tlca.static_lca.lcia(demand={e.input.id: e.amount})\n",
" static_scores[e.input[\"name\"]] = tlca.static_lca.score"
" tlca.base_lca.lcia(demand={e.input.id: e.amount})\n",
" static_scores[e.input[\"name\"]] = tlca.base_lca.score"
]
},
{
Expand Down Expand Up @@ -1638,7 +1638,7 @@
}
],
"source": [
"print(\"Static score: \", sum(static_scores.values())) # should be the same as tlca.static_lca.score\n",
"print(\"Static score: \", sum(static_scores.values())) # should be the same as tlca.base_lca.score\n",
"print(\"Prospective score: \", sum(prospective_scores.values()))\n",
"print(\"Time-explicit score: \", tlca.dynamic_score)"
]
Expand Down
4 changes: 2 additions & 2 deletions notebooks/example_electric_vehicle_standalone.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -1535,7 +1535,7 @@
}
],
"source": [
"tlca.static_lca.score"
"tlca.base_lca.score"
]
},
{
Expand Down Expand Up @@ -1625,7 +1625,7 @@
}
],
"source": [
"print(\"Static score: \", sum(static_scores.values())) # should be the same as tlca.static_lca.score\n",
"print(\"Static score: \", sum(static_scores.values())) # should be the same as tlca.base_lca.score\n",
"print(\"Prospective score: \", sum(prospective_scores.values()))\n",
"print(\"Time-explicit score: \", tlca.dynamic_score)"
]
Expand Down
2 changes: 1 addition & 1 deletion tests/test_dynamic_biomatrix_construction.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def setup_method(self):
def test_identical_time_mapped_producers_exist_in_timeline(self):

duplicates = self.tlca.timeline["time_mapped_producer"].duplicated().any()
assert duplicates, f"No duplicates found in column {"time_mapped_producer"}"
assert duplicates, f"No duplicates found in column time_mapped_producer"

def test_dynamic_biomatrix_for_multiple_identical_time_mapped_producers_in_timeline(self):

Expand Down
4 changes: 2 additions & 2 deletions tests/test_electric_vehicle.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,13 @@ def setup_method(self, vehicle_db):
self.tlca.lci()
self.tlca.static_lcia()

def test_static_lca_score(self):
def test_base_lca_score(self):
slca = bc.LCA({self.electric_vehicle.key: 1}, method=("GWP", "example"))
slca.lci()
slca.lcia()
expected_static_score = slca.score

assert self.tlca.static_lca.score == expected_static_score
assert self.tlca.base_lca.score == expected_static_score

def test_bw_timex_score(self):
ELECTRICITY_CONSUMPTION = 0.2 # kWh/km
Expand Down
3 changes: 2 additions & 1 deletion tests/test_nonunitary_unitprocess.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import math
from datetime import datetime

import bw2data as bd
Expand Down Expand Up @@ -47,4 +48,4 @@ def test_timex_lca_score(self):

print(false_score)

assert self.tlca.score == expected_score
assert math.isclose(self.tlca.static_score, expected_score, rel_tol=1e-9)
2 changes: 1 addition & 1 deletion tests/test_temporal_grouping.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ def test_monthly_resolution_score(self):

expected_score = 1 / 3 * 15 + 1 / 6 * 15 + 1 / 6 * 10 + 1 / 3 * 10

assert self.tlca.score == expected_score
assert self.tlca.static_score == expected_score

def test_daily_resolution_score(self):

Expand Down

0 comments on commit eb901b5

Please sign in to comment.