Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ _WORK IN PROGRESS_

`bw-functional` includes the following built-in allocation functions:

* `manual_allocation`: Does allocation based on the "allocation" field of the Function. Doesn't normalize by amount of production exchange.
* `manual_allocation`: Does allocation based on the "allocation" field of the Product. Doesn't normalize by amount of production exchange.
* `equal`: Splits burdens equally among all functional edges.

You can also do property-based allocation by specifying the property label in the `allocation` field of the Process.
Expand Down
4 changes: 2 additions & 2 deletions bw_functional/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"generic_allocation",
"list_available_properties",
"Process",
"Function",
"Product",
"MFExchange",
"MFExchanges",
"FunctionalSQLiteDatabase",
Expand All @@ -26,7 +26,7 @@
list_available_properties,
)
from .database import FunctionalSQLiteDatabase
from .node_classes import Process, Function
from .node_classes import Process, Product
from .edge_classes import MFExchange, MFExchanges
from .node_dispatch import functional_node_dispatcher
from .utils import allocation_before_writing
Expand Down
66 changes: 33 additions & 33 deletions bw_functional/allocation.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from typing import Callable, List
from logging import getLogger

from .node_classes import Process, Function
from .node_classes import Process, Product

log = getLogger(__name__)

Expand All @@ -20,24 +20,24 @@ def generic_allocation(
"""
Perform allocation for a multifunctional process using a specified getter function.

This function calculates allocation factors for each function in a multifunctional process
based on the values returned by the `getter` function. The allocation factor for each function
This function calculates allocation factors for each product in a multifunctional process
based on the values returned by the `getter` function. The allocation factor for each product
is determined by dividing the value returned by the `getter` function for that function by the
sum of all values returned by the `getter` function for all functions in the process.
sum of all values returned by the `getter` function for all product in the process.

Args:
process (Process): The process object to allocate. Must be an instance of the `Process` class.
getter (Callable): A function that takes a `Function` object and returns a float value
getter (Callable): A function that takes a `Product` object and returns a float value
used for allocation calculations.

Raises:
ValueError: If the `process` is not an instance of the `Process` class.
ZeroDivisionError: If the sum of allocation factors is zero.

Notes:
- Functions with a positive `substitution_factor` and a non-zero `allocation_factor` will
- Products with a positive `substitution_factor` and a non-zero `allocation_factor` will
have their `allocation_factor` reset to 0.0 and will not be included in the allocation.
- Functions with a non-positive `substitution_factor` are included in the allocation.
- Products with a non-positive `substitution_factor` are included in the allocation.
"""
# Ensure the process is a valid Process instance
if not isinstance(process, Process):
Expand All @@ -47,80 +47,80 @@ def generic_allocation(
if not process.multifunctional:
return

# Collect functions eligible for allocation (functions that are not subsituted)
functions = []
for fn in process.functions():
if fn.get("substitution_factor", 0) > 0 and fn["allocation_factor"] > 0:
# Reset allocation factor for functions with positive substitution factor
fn["allocation_factor"] = 0.0
fn.save()
elif fn.get("substitution_factor", 0) <= 0:
# Collect products eligible for allocation (functions that are not subsituted)
products = []
for product in process.products():
if product.get("substitution_factor", 0) > 0 and product["allocation_factor"] > 0:
# Reset allocation factor for products with positive substitution factor
product["allocation_factor"] = 0.0
product.save()
elif product.get("substitution_factor", 0) <= 0:
# Include functions with non-positive substitution factor
functions.append(fn)
products.append(product)

# Calculate the total value for allocation
total = sum([getter(function) for function in functions])
total = sum([getter(product) for product in products])

# Raise an error if the total is zero to avoid division by zero
if not total:
raise ZeroDivisionError("Sum of allocation factors is zero")

# Calculate and assign allocation factors for each function
for i, function in enumerate(functions):
factor = getter(function) / total
function["allocation_factor"] = factor
function.save()
# Calculate and assign allocation factors for each product
for i, product in enumerate(products):
factor = getter(product) / total
product["allocation_factor"] = factor
product.save()


def get_property_value(
function: Function,
product: Product,
property_label: str,
) -> float:
"""
Retrieve the value of a specified property from a given function.

This function extracts the value of a property identified by `property_label` from the
`properties` dictionary of the provided `Function` object. If the property is marked
`properties` dictionary of the provided `Product` object. If the property is marked
as normalized, the value is calculated by multiplying the `amount` in the processing
edge with the `amount` of the property.

Args:
function (Function): The function object from which the property value is retrieved.
Must be an instance of the `Function` class.
product (Product): The product object from which the property value is retrieved.
Must be an instance of the `Product` class.
property_label (str): The label of the property to retrieve.

Returns:
float: The value of the specified property.

Raises:
ValueError: If the provided `function` is not an instance of the `Function` class.
ValueError: If the provided `product` is not an instance of the `Product` class.
KeyError: If the `properties` dictionary is missing or the specified property
is not found in the `properties` dictionary.

Notes:
- If the property is stored as a float (legacy format), a warning is logged.
- If the property is normalized, the value is calculated as:
`function.processing_edge["amount"] * prop["amount"]`.
`product.processing_edge["amount"] * prop["amount"]`.
"""
if not isinstance(function, Function):
if not isinstance(product, Product):
raise ValueError("Passed non-function for allocation")

props = function.get("properties")
props = product.get("properties")

if not props:
raise KeyError(f"Function {function} from process {function.processor} doesn't have properties")
raise KeyError(f"Product {product} from process {product.processor} doesn't have properties")

prop = props.get(property_label)

if not prop:
raise KeyError(f"Function {function} from {function.processor} missing property {property_label}")
raise KeyError(f"Product {product} from {product.processor} missing property {property_label}")

if isinstance(prop, float):
log.warning("Property using legacy float format")
return prop

if prop.get("normalize", False):
return abs(function.processing_edge["amount"]) * prop["amount"]
return abs(product.processing_edge["amount"]) * prop["amount"]
else:
return prop["amount"]

Expand Down
20 changes: 10 additions & 10 deletions bw_functional/custom_allocation.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from bw2data.backends import Exchange

from . import allocation_strategies
from .node_classes import Function, Process
from .node_classes import Product, Process

DEFAULT_ALLOCATIONS = set(allocation_strategies)

Expand Down Expand Up @@ -61,8 +61,8 @@ def list_available_properties(database_label: str, target_process: Optional[Proc
results = {}
all_properties = set()

for function in filter(lambda x: isinstance(x, Function), Database(database_label)):
for key in function.get("properties", {}):
for product in filter(lambda x: isinstance(x, Product), Database(database_label)):
for key in product.get("properties", {}):
all_properties.add(key)

for label in all_properties:
Expand Down Expand Up @@ -95,18 +95,18 @@ def process_property_errors(process: Process, property_label: str) -> List[Prope
if not isinstance(process, Process):
raise TypeError("Node should be the Process type")

for function in process.functions():
properties = function.get("properties", {})
for product in process.products():
properties = product.get("properties", {})
if property_label not in properties:
messages.append(
PropertyMessage(
level=logging.WARNING,
process_id=process.id,
function_id=function.id,
function_id=product.id,
message_type=MessageType.MISSING_FUNCTION_PROPERTY,
message=f"""Function is missing a property value for `{property_label}`.
message=f"""Product is missing a property value for `{property_label}`.
Please define this property for the function:
{function}
{product}
Referenced by multifunctional process:
{process}

Expand All @@ -118,11 +118,11 @@ def process_property_errors(process: Process, property_label: str) -> List[Prope
PropertyMessage(
level=logging.CRITICAL,
process_id=process.id,
function_id=function.id,
function_id=product.id,
message_type=MessageType.NONNUMERIC_FUNCTION_PROPERTY,
message=f"""Found non-numeric value `{properties[property_label]}` in property `{property_label}`.
Please redefine this property for the function:
{function}
{product}
Referenced by multifunctional process:
{process}

Expand Down
2 changes: 1 addition & 1 deletion bw_functional/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ class FunctionalSQLiteDatabase(SQLiteBackend):

This class extends the `SQLiteBackend` to provide additional functionality for
processing and managing processes with one or more functions, including relabeling data, registering
metadata, and processing data into a structured format.
metadata, and processing data into a structured format..
"""

backend = "functional_sqlite"
Expand Down
16 changes: 8 additions & 8 deletions bw_functional/edge_classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ def virtual_edges(self) -> list[dict]:
Raises:
ValueError: If the output is not an instance of the `Process` class.
"""
from .node_classes import Process, Function
from .node_classes import Process, Product
edges = []

if self["type"] == "production":
Expand All @@ -82,10 +82,10 @@ def virtual_edges(self) -> list[dict]:
if not isinstance(self.output, Process):
raise ValueError("Output must be an instance of Process")

for function in self.output.functions():
for product in self.output.products():
ds = deepcopy(self.as_dict())
ds["amount"] = ds["amount"] * function.get("allocation_factor", 1)
ds["output"] = function.key
ds["amount"] = ds["amount"] * product.get("allocation_factor", 1)
ds["output"] = product.key
edges.append(ds)

return edges
Expand All @@ -105,7 +105,7 @@ def save(self, signal: bool = True, data_already_set: bool = False, force_insert
Raises:
NotImplementedError: If parameterization is attempted for production exchanges.
"""
from .node_classes import Process, Function
from .node_classes import Process, Product
log.debug(f"Saving {self['type']} Exchange: {self}")

created = self.id is None # the exchange is new if it has no id
Expand All @@ -122,7 +122,7 @@ def save(self, signal: bool = True, data_already_set: bool = False, force_insert
function = self.input
process = self.output

if not isinstance(process, Process) or not isinstance(function, Function):
if not isinstance(process, Process) or not isinstance(function, Product):
return

if self["type"] == "production":
Expand Down Expand Up @@ -150,15 +150,15 @@ def delete(self, signal: bool = True):
Args:
signal (bool, optional): Whether to send a signal after deletion. Defaults to True.
"""
from .node_classes import Function, Process, MFActivity
from .node_classes import Product, Process, MFActivity
log.debug(f"Deleting {self['type']} Exchange: {self}")

super().delete(signal)

function = self.input
process = self.output

if not isinstance(process, Process) or not isinstance(function, Function):
if not isinstance(process, Process) or not isinstance(function, Product):
return

if self["type"] == "production":
Expand Down
Loading
Loading