Skip to content

Commit

Permalink
Merge pull request #37 from brightway-lca/missing_exchanges
Browse files Browse the repository at this point in the history
Allow matrix edges without database `Exchange` objs
  • Loading branch information
cmutel committed Oct 12, 2023
2 parents 8694d31 + 35a0cb8 commit 95a5e44
Show file tree
Hide file tree
Showing 4 changed files with 218 additions and 40 deletions.
96 changes: 62 additions & 34 deletions bw_temporalis/lca.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ class MultipleTechnosphereExchanges(Exception):
pass


class NoExchange:
"""The edge was created dynamically via a datapackage. There is no edge in the database."""

pass


class TemporalisLCA:
"""
Calculate an LCA using graph traversal, with edges using temporal distributions.
Expand Down Expand Up @@ -205,51 +211,67 @@ def build_timeline(self, node_timeline: bool | None = False) -> Timeline:

def _exchange_value(
self,
exchange: bd.backends.ExchangeDataset,
exchange: Union[bd.backends.ExchangeDataset, NoExchange],
row_id: int,
col_id: int,
matrix_label: str,
) -> Union[float, TemporalDistribution]:
from . import loader_registry

td = exchange.data.get("temporal_distribution")
if isinstance(td, str) and "__loader__" in td:
data = json.loads(td)
try:
td = loader_registry[td["__loader__"]](data)
except KeyError:
raise KeyError(
"Can't find correct loader {} in `loader_registry`".format(
td["__loader__"]
if exchange is NoExchange:
td = None
else:
td = exchange.data.get("temporal_distribution")
if isinstance(td, str) and "__loader__" in td:
data = json.loads(td)
try:
td = loader_registry[td["__loader__"]](data)
except KeyError:
raise KeyError(
"Can't find correct loader {} in `loader_registry`".format(
td["__loader__"]
)
)
elif not (isinstance(td, (TemporalDistribution, TDAware)) or td is None):
raise ValueError(
f"Can't understand value for `temporal_distribution` in exchange {exchange}"
)
elif not (isinstance(td, (TemporalDistribution, TDAware)) or td is None):
raise ValueError(
f"Can't understand value for `temporal_distribution` in exchange {exchange}"
)

sign = (
1
if exchange.data["type"] not in ("generic consumption", "technosphere")
else -1
)
sign = (
1
if exchange.data["type"] not in ("generic consumption", "technosphere")
else -1
)

if matrix_label == "technosphere_matrix":
amount = (
sign
* self.lca_object.technosphere_matrix[
self.lca_object.dicts.product[row_id],
self.lca_object.dicts.activity[col_id],
]
)
value = self.lca_object.technosphere_matrix[
self.lca_object.dicts.product[row_id],
self.lca_object.dicts.activity[col_id],
]
if exchange is NoExchange:
# Assume technosphere input so negative sign, unless we have a
# positive value and the number is on the diagonal, or the
# IDs are the same (shared product/process, not on the diagonal
# for whatever reason).
if row_id == col_id:
sign = 1
elif (
value > 0
and self.lca_object.dicts.biosphere[row_id]
== self.lca_object.dicts.activity[col_id]
):
sign = 1
else:
sign = -1

amount = sign * value
elif matrix_label == "biosphere_matrix":
amount = (
exchange.data.get("fraction", 1)
* self.lca_object.biosphere_matrix[
self.lca_object.dicts.biosphere[row_id],
self.lca_object.dicts.activity[col_id],
]
)
exchange.data.get("fraction", 1) if exchange is not NoExchange else 1
) * self.lca_object.biosphere_matrix[
self.lca_object.dicts.biosphere[row_id],
self.lca_object.dicts.activity[col_id],
]
else:
raise ValueError(f"Unknown matrix type {matrix_label}")

Expand Down Expand Up @@ -277,8 +299,10 @@ def get_biosphere_exchanges(self, flow_id: int, activity_id: int) -> Iterable[ED
for exc in exchanges:
exc.data["fraction"] = exc.data["amount"] / total
yield exc
else:
elif len(exchanges) == 1:
yield from exchanges
else:
yield NoExchange

def get_technosphere_exchange(self, input_id: int, output_id: int) -> ED:
def printer(x):
Expand All @@ -293,4 +317,8 @@ def printer(x):
printer(exchanges[0].output),
)
)
return exchanges[0]
elif not exchanges:
# Edge injected via datapackage, no exchange in dataset
return NoExchange
else:
return exchanges[0]
4 changes: 2 additions & 2 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ python_requires = >=3.8
# new major versions. This works if the required packages follow Semantic Versioning.
# For more information, check out https://semver.org/.
install_requires =
bw2calc >=2.0.dev12
bw2data >=4.0.dev18
bw2calc >=2.0.dev13
bw2data >=4.0.dev32
bw_graph_tools
numpy
pandas
Expand Down
4 changes: 0 additions & 4 deletions tests/test_lca.py
Original file line number Diff line number Diff line change
Expand Up @@ -439,9 +439,5 @@ def test_temporalis_lca_draw_from_matrix(basic_db):
},
]

import pprint

for a in expected_nodes:
pprint.pprint(tlca.nodes[a["unique_id"]])
pprint.pprint(a)
node_equal_dict(tlca.nodes[a["unique_id"]], a)
154 changes: 154 additions & 0 deletions tests/test_patched_matrix_lca.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import uuid

import bw2calc as bc
import bw2data as bd
import bw_processing as bwp
import numpy as np
import pytest
from bw2data.tests import bw2test

from bw_temporalis import TemporalisLCA


def safety_razor(
consumer: bd.Node,
previous_producer: bd.Node,
new_producer: bd.Node,
) -> bwp.Datapackage:
amount = sum(
exc["amount"]
for exc in consumer.technosphere()
if exc.input == previous_producer
)

datapackage = bwp.create_datapackage()
datapackage.add_persistent_vector(
# This function would need to be adapted for biosphere edges
matrix="technosphere_matrix",
name=uuid.uuid4().hex,
data_array=np.array([0, amount], dtype=float),
indices_array=np.array(
[(previous_producer.id, consumer.id), (new_producer.id, consumer.id)],
dtype=bwp.INDICES_DTYPE,
),
flip_array=np.array([False, True], dtype=bool),
)
return datapackage


@pytest.fixture
@bw2test
def patched_matrix():
db = bd.Database("f")
db.write(
{
("f", "0"): {},
("f", "1"): {
"exchanges": [
{
"type": "technosphere",
"input": ("f", "2"),
"amount": 2,
},
{
"type": "production",
"input": ("f", "1"),
"amount": 1,
},
]
},
("f", "2"): {
"exchanges": [
{
"type": "technosphere",
"input": ("f", "3"),
"amount": 4,
},
{
"type": "production",
"input": ("f", "2"),
"amount": 1,
},
{
"type": "biosphere",
"amount": 1,
"input": ("f", "0"),
},
]
},
("f", "3"): {
"exchanges": [
{
"type": "production",
"input": ("f", "3"),
"amount": 1,
},
{
"type": "biosphere",
"amount": 10,
"input": ("f", "0"),
},
]
},
}
)
f1 = bd.get_node(code="1")
f2 = bd.get_node(code="2")
f3 = bd.get_node(code="3")
dp = safety_razor(
consumer=f1,
previous_producer=f2,
new_producer=f3,
)
bd.Method(("m",)).write(
[
(("f", "0"), 1),
]
)
fu, data_objs, remapping = bd.prepare_lca_inputs(demand={f1: 1}, method=("m",))
return fu, data_objs + [dp]


@pytest.fixture
def lca(patched_matrix):
lca = bc.LCA(demand=patched_matrix[0], data_objs=patched_matrix[1])
lca.lci()
lca.lcia()
return lca


def test_pmlca_without_modification(patched_matrix):
lca = bc.LCA({bd.get_node(code="1"): 1}, ("m",))
lca.lci()
lca.lcia()
assert lca.score == 82


def test_pmlca_basic(lca):
assert lca.score == 20


def test_pmlca_values(lca):
tlca = TemporalisLCA(
lca_object=lca,
starting_datetime="2023-01-01",
)
tl = tlca.build_timeline()
assert len(tl.data) == 1
ftd = tl.data[0]
assert ftd.flow == bd.get_node(code="0").id
assert ftd.activity == bd.get_node(code="3").id
assert ftd.distribution.amount.sum() == 20


def test_pmlca_dataframe(lca):
tlca = TemporalisLCA(
lca_object=lca,
starting_datetime="2023-01-01",
)
tl = tlca.build_timeline()
df = tl.build_dataframe()
assert str(df.date[0]) == "2023-01-01 00:00:00"
assert df.amount[0] == 20
assert df.flow[0] == bd.get_node(code="0").id
assert df.activity[0] == bd.get_node(code="3").id

0 comments on commit 95a5e44

Please sign in to comment.