bw-functional 0b79__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bw_functional/__init__.py +65 -0
- bw_functional/allocation.py +142 -0
- bw_functional/convert.py +143 -0
- bw_functional/database.py +514 -0
- bw_functional/edge_classes.py +166 -0
- bw_functional/node_classes.py +591 -0
- bw_functional-0b79.dist-info/METADATA +216 -0
- bw_functional-0b79.dist-info/RECORD +11 -0
- bw_functional-0b79.dist-info/WHEEL +5 -0
- bw_functional-0b79.dist-info/licenses/LICENSE +12 -0
- bw_functional-0b79.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
__all__ = (
|
|
2
|
+
"__version__",
|
|
3
|
+
"allocation_strategies",
|
|
4
|
+
"generic_allocation",
|
|
5
|
+
"Process",
|
|
6
|
+
"Product",
|
|
7
|
+
"MFExchange",
|
|
8
|
+
"MFExchanges",
|
|
9
|
+
"FunctionalSQLiteDatabase",
|
|
10
|
+
"property_allocation",
|
|
11
|
+
)
|
|
12
|
+
# import os
|
|
13
|
+
__version__ = "0.0.2"
|
|
14
|
+
|
|
15
|
+
from logging import getLogger
|
|
16
|
+
|
|
17
|
+
from bw2data import labels
|
|
18
|
+
from bw2data.subclass_mapping import DATABASE_BACKEND_MAPPING, NODE_PROCESS_CLASS_MAPPING
|
|
19
|
+
|
|
20
|
+
from .allocation import allocation_strategies, generic_allocation, property_allocation
|
|
21
|
+
from .database import FunctionalSQLiteDatabase
|
|
22
|
+
from .node_classes import Process, Product
|
|
23
|
+
from .edge_classes import MFExchange, MFExchanges
|
|
24
|
+
|
|
25
|
+
log = getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
DATABASE_BACKEND_MAPPING["functional_sqlite"] = FunctionalSQLiteDatabase
|
|
28
|
+
NODE_PROCESS_CLASS_MAPPING["functional_sqlite"] = FunctionalSQLiteDatabase.node_class
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
if "waste" not in labels.node_types:
|
|
32
|
+
labels.lci_node_types.append("waste")
|
|
33
|
+
if "nonfunctional" not in labels.node_types:
|
|
34
|
+
labels.other_node_types.append("nonfunctional")
|
|
35
|
+
|
|
36
|
+
# make sure allocation happens on parameter changes
|
|
37
|
+
def _init_signals():
|
|
38
|
+
from bw2data.signals import on_activity_parameter_recalculate
|
|
39
|
+
|
|
40
|
+
on_activity_parameter_recalculate.connect(_check_parameterized_exchange_for_allocation)
|
|
41
|
+
|
|
42
|
+
def _check_parameterized_exchange_for_allocation(_, name):
|
|
43
|
+
import bw2data as bd
|
|
44
|
+
from bw2data.parameters import ParameterizedExchange
|
|
45
|
+
from bw2data.backends import ExchangeDataset
|
|
46
|
+
|
|
47
|
+
databases = [k for k, v in bd.databases.items() if v["backend"] == "functional_sqlite"]
|
|
48
|
+
|
|
49
|
+
p_exchanges = ParameterizedExchange.select().where(ParameterizedExchange.group==name)
|
|
50
|
+
exc_ids = [p_exc.exchange for p_exc in p_exchanges]
|
|
51
|
+
exchanges = ExchangeDataset.select(ExchangeDataset.output_database, ExchangeDataset.output_code).where(
|
|
52
|
+
(ExchangeDataset.id.in_(exc_ids)) &
|
|
53
|
+
(ExchangeDataset.type == "production") &
|
|
54
|
+
(ExchangeDataset.output_database.in_(databases))
|
|
55
|
+
)
|
|
56
|
+
process_keys = set(exchanges.tuples())
|
|
57
|
+
|
|
58
|
+
for key in process_keys:
|
|
59
|
+
process = bd.get_activity(key)
|
|
60
|
+
if not isinstance(process, Process):
|
|
61
|
+
log.warning(f"Process {key} is not an instance of Process, skipping allocation check.")
|
|
62
|
+
continue
|
|
63
|
+
process.allocate()
|
|
64
|
+
|
|
65
|
+
_init_signals()
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
from functools import partial
|
|
2
|
+
from typing import Callable, List
|
|
3
|
+
from logging import getLogger
|
|
4
|
+
|
|
5
|
+
from .node_classes import Process, Product
|
|
6
|
+
|
|
7
|
+
log = getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def generic_allocation(
|
|
11
|
+
process: Process,
|
|
12
|
+
getter: lambda function: float(),
|
|
13
|
+
) -> None:
|
|
14
|
+
"""
|
|
15
|
+
Perform allocation for a multifunctional process using a specified getter function.
|
|
16
|
+
|
|
17
|
+
This function calculates allocation factors for each product in a multifunctional process
|
|
18
|
+
based on the values returned by the `getter` function. The allocation factor for each product
|
|
19
|
+
is determined by dividing the value returned by the `getter` function for that function by the
|
|
20
|
+
sum of all values returned by the `getter` function for all product in the process.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
process (Process): The process object to allocate. Must be an instance of the `Process` class.
|
|
24
|
+
getter (Callable): A function that takes a `Product` object and returns a float value
|
|
25
|
+
used for allocation calculations.
|
|
26
|
+
|
|
27
|
+
Raises:
|
|
28
|
+
ValueError: If the `process` is not an instance of the `Process` class.
|
|
29
|
+
ZeroDivisionError: If the sum of allocation factors is zero.
|
|
30
|
+
|
|
31
|
+
Notes:
|
|
32
|
+
- Products with a positive `substitution_factor` and a non-zero `allocation_factor` will
|
|
33
|
+
have their `allocation_factor` reset to 0.0 and will not be included in the allocation.
|
|
34
|
+
- Products with a non-positive `substitution_factor` are included in the allocation.
|
|
35
|
+
"""
|
|
36
|
+
# Ensure the process is a valid Process instance
|
|
37
|
+
if not isinstance(process, Process):
|
|
38
|
+
raise ValueError("Activity must be a Process instance")
|
|
39
|
+
|
|
40
|
+
# Return none if the process is not multifunctional
|
|
41
|
+
if not process.multifunctional:
|
|
42
|
+
return
|
|
43
|
+
|
|
44
|
+
# Collect products eligible for allocation (functions that are not subsituted)
|
|
45
|
+
products = []
|
|
46
|
+
for product in process.products():
|
|
47
|
+
if product.get("substitution_factor", 0) > 0 and product["allocation_factor"] > 0:
|
|
48
|
+
# Reset allocation factor for products with positive substitution factor
|
|
49
|
+
product["allocation_factor"] = 0.0
|
|
50
|
+
product.save()
|
|
51
|
+
elif product.get("substitution_factor", 0) <= 0:
|
|
52
|
+
# Include functions with non-positive substitution factor
|
|
53
|
+
products.append(product)
|
|
54
|
+
|
|
55
|
+
# Calculate the total value for allocation
|
|
56
|
+
total = sum([getter(product) for product in products])
|
|
57
|
+
|
|
58
|
+
# Raise an error if the total is zero to avoid division by zero
|
|
59
|
+
if not total:
|
|
60
|
+
raise ZeroDivisionError("Sum of allocation factors is zero")
|
|
61
|
+
|
|
62
|
+
# Calculate and assign allocation factors for each product
|
|
63
|
+
for i, product in enumerate(products):
|
|
64
|
+
factor = getter(product) / total
|
|
65
|
+
product["allocation_factor"] = factor
|
|
66
|
+
product.save()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def get_property_value(
|
|
70
|
+
product: Product,
|
|
71
|
+
property_label: str,
|
|
72
|
+
) -> float:
|
|
73
|
+
"""
|
|
74
|
+
Retrieve the value of a specified property from a given function.
|
|
75
|
+
|
|
76
|
+
This function extracts the value of a property identified by `property_label` from the
|
|
77
|
+
`properties` dictionary of the provided `Product` object. If the property is marked
|
|
78
|
+
as normalized, the value is calculated by multiplying the `amount` in the processing
|
|
79
|
+
edge with the `amount` of the property.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
product (Product): The product object from which the property value is retrieved.
|
|
83
|
+
Must be an instance of the `Product` class.
|
|
84
|
+
property_label (str): The label of the property to retrieve.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
float: The value of the specified property.
|
|
88
|
+
|
|
89
|
+
Raises:
|
|
90
|
+
ValueError: If the provided `product` is not an instance of the `Product` class.
|
|
91
|
+
KeyError: If the `properties` dictionary is missing or the specified property
|
|
92
|
+
is not found in the `properties` dictionary.
|
|
93
|
+
|
|
94
|
+
Notes:
|
|
95
|
+
- If the property is stored as a float (legacy format), a warning is logged.
|
|
96
|
+
- If the property is normalized, the value is calculated as:
|
|
97
|
+
`product.processing_edge["amount"] * prop["amount"]`.
|
|
98
|
+
"""
|
|
99
|
+
if not isinstance(product, Product):
|
|
100
|
+
raise ValueError("Passed non-function for allocation")
|
|
101
|
+
|
|
102
|
+
props = product.get("properties")
|
|
103
|
+
|
|
104
|
+
if not props:
|
|
105
|
+
raise KeyError(f"Product {product} from process {product.processor} doesn't have properties")
|
|
106
|
+
|
|
107
|
+
prop = props.get(property_label)
|
|
108
|
+
|
|
109
|
+
if not prop:
|
|
110
|
+
raise KeyError(f"Product {product} from {product.processor} missing property {property_label}")
|
|
111
|
+
|
|
112
|
+
if isinstance(prop, float):
|
|
113
|
+
log.warning("Property using legacy float format")
|
|
114
|
+
return prop
|
|
115
|
+
|
|
116
|
+
if prop.get("normalize", False):
|
|
117
|
+
return abs(product.processing_edge["amount"]) * prop["amount"]
|
|
118
|
+
else:
|
|
119
|
+
return prop["amount"]
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def property_allocation(property_label: str) -> Callable:
|
|
123
|
+
"""
|
|
124
|
+
Create a partial function for allocating a process based on a specific property.
|
|
125
|
+
|
|
126
|
+
This function generates a callable that performs allocation for a multifunctional process
|
|
127
|
+
using a getter function that retrieves the value of the specified property.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
property_label (str): The label of the property to be used for allocation.
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
Callable: A partial function that performs allocation using the specified property.
|
|
134
|
+
"""
|
|
135
|
+
getter = partial(get_property_value, property_label=property_label)
|
|
136
|
+
return partial(generic_allocation, getter=getter)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
allocation_strategies = {
|
|
140
|
+
"equal": partial(generic_allocation, getter=lambda x: 1.0),
|
|
141
|
+
"manual": partial(generic_allocation, getter=lambda x: x.get("allocation_factor", 1)),
|
|
142
|
+
}
|
bw_functional/convert.py
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import tqdm
|
|
2
|
+
from logging import getLogger
|
|
3
|
+
|
|
4
|
+
import bw2data as bd
|
|
5
|
+
from bw2data.backends import SQLiteBackend
|
|
6
|
+
|
|
7
|
+
from .database import FunctionalSQLiteDatabase
|
|
8
|
+
|
|
9
|
+
log = getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def convert_sqlite_to_functional_sqlite(database_name: str) -> dict:
|
|
13
|
+
return SQLiteToFunctionalSQLite.convert(database_name)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SQLiteToFunctionalSQLite:
|
|
17
|
+
@classmethod
|
|
18
|
+
def convert(cls, database_name: str):
|
|
19
|
+
db = bd.Database(database_name)
|
|
20
|
+
|
|
21
|
+
if not isinstance(db, SQLiteBackend):
|
|
22
|
+
raise TypeError("Database is not of type SQLite.")
|
|
23
|
+
|
|
24
|
+
converted = {}
|
|
25
|
+
|
|
26
|
+
for key, ds in tqdm.tqdm(db.load().items()):
|
|
27
|
+
if ds["type"] in ["process", "processwithreferenceproduct"]:
|
|
28
|
+
converted.update(cls.convert_process(key, ds))
|
|
29
|
+
|
|
30
|
+
return converted
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
def convert_process(cls, key, ds):
|
|
34
|
+
ds["type"] = "process"
|
|
35
|
+
production = [x for x in enumerate(ds["exchanges"]) if x[1]["type"] == "production"]
|
|
36
|
+
if len(production) > 1:
|
|
37
|
+
# Check if the process has multiple production exchanges
|
|
38
|
+
# and raise an error if so
|
|
39
|
+
act = bd.get_activity(key)
|
|
40
|
+
raise ValueError("Cannot convert a process with multiple production exchanges to functional_sqlite.", act)
|
|
41
|
+
|
|
42
|
+
cls.convert_exchanges(key, ds)
|
|
43
|
+
|
|
44
|
+
if not production:
|
|
45
|
+
function_key, function = cls.create_function(key, ds)
|
|
46
|
+
else:
|
|
47
|
+
index, exchange = production.pop()
|
|
48
|
+
function_key, function = cls.create_function(key, ds, amount=exchange["amount"])
|
|
49
|
+
ds["exchanges"].pop(index)
|
|
50
|
+
|
|
51
|
+
return {key: ds, function_key: function}
|
|
52
|
+
|
|
53
|
+
@staticmethod
|
|
54
|
+
def create_function(key, ds, amount=1.0, name=None):
|
|
55
|
+
function_name = name or ds.get("reference product") or ds.get("product") or ds.get("name")
|
|
56
|
+
function_code = ds["code"] + "_function"
|
|
57
|
+
function_key = (key[0], function_code)
|
|
58
|
+
|
|
59
|
+
function = {
|
|
60
|
+
"type": "product" if amount > 0 else "waste",
|
|
61
|
+
"name": function_name,
|
|
62
|
+
"exchanges": [],
|
|
63
|
+
"database": ds["database"],
|
|
64
|
+
"code": function_code,
|
|
65
|
+
"processor": key,
|
|
66
|
+
"location": ds.get("location"),
|
|
67
|
+
"unit": ds.get("unit"),
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
ds["exchanges"].append({
|
|
71
|
+
"type": "production",
|
|
72
|
+
"input": function_key,
|
|
73
|
+
"output": key,
|
|
74
|
+
"amount": amount,
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
return function_key, function
|
|
78
|
+
|
|
79
|
+
@staticmethod
|
|
80
|
+
def convert_exchanges(key, ds) -> None:
|
|
81
|
+
excs = ds["exchanges"]
|
|
82
|
+
database, code = key
|
|
83
|
+
|
|
84
|
+
for exc in excs:
|
|
85
|
+
if exc["input"][0] != database:
|
|
86
|
+
continue
|
|
87
|
+
exc["input"] = (database, exc["input"][1] + "_function")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def convert_functional_sqlite_to_sqlite(database_name: str):
|
|
91
|
+
return FunctionalSQLiteToSQLite.convert(database_name)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class FunctionalSQLiteToSQLite:
|
|
95
|
+
@classmethod
|
|
96
|
+
def convert(cls, database_name: str):
|
|
97
|
+
db = bd.Database(database_name)
|
|
98
|
+
|
|
99
|
+
if not isinstance(db, FunctionalSQLiteDatabase):
|
|
100
|
+
raise TypeError("Database is not of type functional_sqlite.")
|
|
101
|
+
|
|
102
|
+
raw = db.load()
|
|
103
|
+
converted = {}
|
|
104
|
+
|
|
105
|
+
for key, ds in tqdm.tqdm(raw.items()):
|
|
106
|
+
if ds["type"] in ["product", "waste"]:
|
|
107
|
+
processor = raw[ds["processor"]]
|
|
108
|
+
converted[key] = cls.convert_function(key, ds, processor)
|
|
109
|
+
|
|
110
|
+
return converted
|
|
111
|
+
|
|
112
|
+
@classmethod
|
|
113
|
+
def convert_function(cls, key, ds, processor):
|
|
114
|
+
ds["type"] = "processwithreferenceproduct"
|
|
115
|
+
ds["product"] = ds["name"]
|
|
116
|
+
ds["name"] = processor["name"]
|
|
117
|
+
|
|
118
|
+
ds["exchanges"] = []
|
|
119
|
+
del ds["processor"]
|
|
120
|
+
|
|
121
|
+
for exc in processor["exchanges"]:
|
|
122
|
+
exc = exc.copy()
|
|
123
|
+
exc["output"] = key
|
|
124
|
+
|
|
125
|
+
if exc["type"] == "production" and exc["input"] != key:
|
|
126
|
+
# production exchange of another function than this one
|
|
127
|
+
continue
|
|
128
|
+
|
|
129
|
+
if exc["type"] != "production" and ds.get("allocation_factor"):
|
|
130
|
+
log.info(f"Allocating exchange from {exc['input']} to {ds['name']} "
|
|
131
|
+
f"with factor {ds['allocation_factor']}")
|
|
132
|
+
exc["amount"] = exc["amount"] * ds['allocation_factor']
|
|
133
|
+
if exc.get("formula"):
|
|
134
|
+
log.info(f"Allocating formula from {exc['input']} to {ds['name']}: "
|
|
135
|
+
f"{exc['formula']} * {ds['allocation_factor']}")
|
|
136
|
+
exc["formula"] = f"{exc['formula']} * {ds['allocation_factor']}"
|
|
137
|
+
if exc.get("uncertainty type"):
|
|
138
|
+
log.warning(f"Exchange from {exc['input']} to {ds['name']} has an uncertainty distribution that "
|
|
139
|
+
f"will not be allocated")
|
|
140
|
+
|
|
141
|
+
ds["exchanges"].append(exc)
|
|
142
|
+
|
|
143
|
+
return ds
|