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.
@@ -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
+ }
@@ -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