Skip to content

Commit dedc3d5

Browse files
committed
Add create_products_as_new_nodes strategy
1 parent 9ecda28 commit dedc3d5

File tree

4 files changed

+306
-0
lines changed

4 files changed

+306
-0
lines changed

CHANGES.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# `bw2io` Changelog
22

3+
### DEV
4+
5+
* Add `create_products_as_new_nodes` strategy
6+
37
### 0.9.DEV37 (2024-09-04)
48

59
* Fix out of order but with `create_randonneur_excel_template_for_unlinked`

bw2io/strategies/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"convert_activity_parameters_to_list",
1010
"convert_uncertainty_types_to_integers",
1111
"create_composite_code",
12+
"create_products_as_new_nodes",
1213
"csv_add_missing_exchanges_section",
1314
"csv_drop_unknown",
1415
"csv_numerize",
@@ -175,6 +176,7 @@
175176
)
176177
from .locations import update_ecoinvent_locations
177178
from .migrations import migrate_datasets, migrate_exchanges
179+
from .products import create_products_as_new_nodes
178180
from .sentier import match_internal_simapro_simapro_with_unit_conversion
179181
from .simapro import (
180182
change_electricity_unit_mj_to_kwh,

bw2io/strategies/products.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
from pprint import pformat
2+
from uuid import uuid4
3+
from typing import List
4+
import bw2data as bd
5+
6+
7+
EDGE_CORE_COLUMNS = [
8+
"name",
9+
"amount",
10+
"database",
11+
"location",
12+
"unit",
13+
"functional",
14+
"type",
15+
"uncertainty type",
16+
"loc",
17+
"scale",
18+
"shape",
19+
"minimum",
20+
"maximum",
21+
]
22+
23+
24+
def create_products_as_new_nodes(data: List[dict]) -> List[dict]:
25+
"""Create new product nodes and link to them if needed.
26+
27+
We create new `product` if the following conditions are met:
28+
29+
* The edge is functional (`obj.get("functional") is True`)
30+
* The edge is unlinked (`obj.get("input")` is falsey)
31+
* The given edge has a `name`, and that `name` is different than the dataset `name`
32+
* The combination of `name` and `location` is not present in the other dataset nodes. If no
33+
`location` attribute is given for the edge under consideration, we use the `location` of the
34+
dataset.
35+
36+
Create new nodes, and links the originating edges to the new product nodes.
37+
38+
Modifies data in-place, and returns the modified `data`.
39+
40+
"""
41+
combos = {(ds.get("name"), ds.get("location")) for ds in data}
42+
nodes = []
43+
44+
for ds in data:
45+
for edge in ds.get('exchanges', []):
46+
if edge.get('functional') and not edge.get('input') and edge.get('name') and edge['name'] != ds.get('name'):
47+
if not ds.get("database"):
48+
raise KeyError("""
49+
Can't create a new `product` node, as dataset is missing `database` attribute:
50+
{}""".format(pformat(ds)))
51+
key = (edge['name'], edge.get('location') or ds.get('location'))
52+
if key not in combos:
53+
code = uuid4().hex
54+
nodes.append({
55+
'name': edge['name'],
56+
'location': key[1] or bd.config.global_location,
57+
'unit': edge.get('unit') or ds.get('unit'),
58+
'exchanges': [],
59+
'code': code,
60+
'type': bd.labels.product_node_default,
61+
'database': ds['database'],
62+
} | {k: v for k, v in edge.items() if k not in EDGE_CORE_COLUMNS})
63+
edge['input'] = (ds['database'], code)
64+
combos.add(key)
65+
66+
if nodes:
67+
data.extend(nodes)
68+
return data

tests/strategies/test_products.py

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
import bw2data as bd
2+
from copy import deepcopy
3+
import pytest
4+
from bw2io.strategies import create_products_as_new_nodes
5+
6+
7+
def test_create_products_as_new_nodes_basic():
8+
data = [{
9+
'name': 'epsilon',
10+
'location': 'there',
11+
}, {
12+
'name': 'alpha',
13+
'database': 'foo',
14+
'exchanges': [{
15+
'name': 'beta',
16+
'unit': 'kg',
17+
'location': 'here',
18+
'functional': True,
19+
'type': 'technosphere',
20+
'extra': True,
21+
}]
22+
}]
23+
original = deepcopy(data)
24+
result = create_products_as_new_nodes(data)
25+
assert len(data) == 3
26+
original[1]['exchanges'][0]['input'] = (result[2]['database'], result[2]['code'])
27+
assert result[:2] == original[:2]
28+
product = {
29+
'database': 'foo',
30+
'code': result[2]['code'],
31+
'name': 'beta',
32+
'unit': 'kg',
33+
'location': 'here',
34+
'exchanges': [],
35+
'type': bd.labels.product_node_default,
36+
'extra': True,
37+
}
38+
assert result[2] == product
39+
40+
41+
def test_create_products_as_new_nodes_skip_nonqualifying():
42+
data = [{
43+
'name': 'epsilon',
44+
'location': 'there',
45+
}, {
46+
'name': 'alpha',
47+
'database': 'foo',
48+
'exchanges': [{
49+
'name': 'beta',
50+
'unit': 'kg',
51+
'location': 'here',
52+
'functional': True,
53+
'type': 'technosphere',
54+
'extra': True,
55+
}, {
56+
'unit': 'kg',
57+
'location': 'here',
58+
'functional': True,
59+
'type': 'technosphere',
60+
'extra': True,
61+
}, {
62+
'name': 'gamma',
63+
'unit': 'kg',
64+
'location': 'here',
65+
'functional': False,
66+
'type': 'production',
67+
'extra': True,
68+
}, {
69+
'name': 'delta',
70+
'unit': 'kg',
71+
'location': 'here',
72+
'functional': True,
73+
'type': 'technosphere',
74+
'input': ("foo", "bar"),
75+
}, {
76+
'name': 'epsilon',
77+
'unit': 'kg',
78+
'location': 'there',
79+
'functional': True,
80+
'type': 'technosphere',
81+
}]
82+
}]
83+
original = deepcopy(data)
84+
result = create_products_as_new_nodes(data)
85+
assert len(data) == 3
86+
original[1]['exchanges'][0]['input'] = (result[2]['database'], result[2]['code'])
87+
assert result[:2] == original[:2]
88+
assert result[2]['name'] == 'beta'
89+
90+
91+
def test_create_products_as_new_nodes_duplicate_exchanges():
92+
data = [{
93+
'name': 'alpha',
94+
'database': 'foo',
95+
'exchanges': [{
96+
'name': 'beta',
97+
'unit': 'kg',
98+
'location': 'here',
99+
'functional': True,
100+
'type': 'technosphere',
101+
'extra': True,
102+
'amount': 7,
103+
}, {
104+
'name': 'beta',
105+
'unit': 'kg',
106+
'location': 'here',
107+
'functional': True,
108+
'type': 'technosphere',
109+
'extra': True,
110+
'amount': 17,
111+
}]
112+
}]
113+
result = create_products_as_new_nodes(data)
114+
assert len(data) == 2
115+
assert result[1]['name'] == 'beta'
116+
117+
118+
def test_create_products_as_new_nodes_inherit_process_location():
119+
data = [{
120+
'name': 'alpha',
121+
'database': 'foo',
122+
'location': 'here',
123+
'exchanges': [{
124+
'name': 'beta',
125+
'unit': 'kg',
126+
'functional': True,
127+
'type': 'technosphere',
128+
'extra': True,
129+
}]
130+
}]
131+
result = create_products_as_new_nodes(data)
132+
assert len(data) == 2
133+
product = {
134+
'database': 'foo',
135+
'code': result[1]['code'],
136+
'name': 'beta',
137+
'unit': 'kg',
138+
'location': 'here',
139+
'exchanges': [],
140+
'type': bd.labels.product_node_default,
141+
'extra': True,
142+
}
143+
assert result[1] == product
144+
145+
146+
def test_create_products_as_new_nodes_inherit_process_unit():
147+
data = [{
148+
'name': 'alpha',
149+
'database': 'foo',
150+
'unit': 'kg',
151+
'exchanges': [{
152+
'name': 'beta',
153+
'location': 'here',
154+
'functional': True,
155+
'type': 'technosphere',
156+
'extra': True,
157+
}]
158+
}]
159+
result = create_products_as_new_nodes(data)
160+
assert len(data) == 2
161+
product = {
162+
'database': 'foo',
163+
'code': result[1]['code'],
164+
'name': 'beta',
165+
'unit': 'kg',
166+
'location': 'here',
167+
'exchanges': [],
168+
'type': bd.labels.product_node_default,
169+
'extra': True,
170+
}
171+
assert result[1] == product
172+
173+
174+
def test_create_products_as_new_nodes_inherit_process_location_when_searching():
175+
data = [{
176+
'name': 'beta',
177+
'location': 'here',
178+
}, {
179+
'name': 'alpha',
180+
'database': 'foo',
181+
'location': 'here',
182+
'exchanges': [{
183+
'name': 'beta',
184+
'unit': 'kg',
185+
'functional': True,
186+
'type': 'technosphere',
187+
'extra': True,
188+
}]
189+
}]
190+
create_products_as_new_nodes(data)
191+
assert len(data) == 2
192+
193+
194+
def test_create_products_as_new_nodes_get_default_global_location():
195+
data = [{
196+
'name': 'alpha',
197+
'database': 'foo',
198+
'exchanges': [{
199+
'name': 'beta',
200+
'unit': 'kg',
201+
'functional': True,
202+
'type': 'technosphere',
203+
'extra': True,
204+
}]
205+
}]
206+
result = create_products_as_new_nodes(data)
207+
assert len(data) == 2
208+
product = {
209+
'database': 'foo',
210+
'code': result[1]['code'],
211+
'name': 'beta',
212+
'unit': 'kg',
213+
'location': bd.config.global_location,
214+
'exchanges': [],
215+
'type': bd.labels.product_node_default,
216+
'extra': True,
217+
}
218+
assert result[1] == product
219+
220+
221+
def test_create_products_as_new_nodes_dataset_must_have_database_key():
222+
data = [{
223+
'name': 'alpha',
224+
'exchanges': [{
225+
'name': 'beta',
226+
'unit': 'kg',
227+
'functional': True,
228+
'type': 'technosphere',
229+
}]
230+
}]
231+
with pytest.raises(KeyError):
232+
create_products_as_new_nodes(data)

0 commit comments

Comments
 (0)