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
14 changes: 7 additions & 7 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 @@ -27,7 +27,7 @@ def generic_allocation(

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:
Expand Down Expand Up @@ -73,7 +73,7 @@ def generic_allocation(


def get_property_value(
function: Function,
function: Product,
property_label: str,
) -> float:
"""
Expand All @@ -85,7 +85,7 @@ def get_property_value(
edge with the `amount` of the property.

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

Expand All @@ -102,18 +102,18 @@ def get_property_value(
- If the property is normalized, the value is calculated as:
`function.processing_edge["amount"] * prop["amount"]`.
"""
if not isinstance(function, Function):
if not isinstance(function, Product):
raise ValueError("Passed non-function for allocation")

props = function.get("properties")

if not props:
raise KeyError(f"Function {function} from process {function.processor} doesn't have properties")
raise KeyError(f"Product {function} from process {function.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 {function} from {function.processor} missing property {property_label}")

if isinstance(prop, float):
log.warning("Property using legacy float format")
Expand Down
6 changes: 3 additions & 3 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,7 +61,7 @@ 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 function in filter(lambda x: isinstance(x, Product), Database(database_label)):
for key in function.get("properties", {}):
all_properties.add(key)

Expand Down Expand Up @@ -104,7 +104,7 @@ def process_property_errors(process: Process, property_label: str) -> List[Prope
process_id=process.id,
function_id=function.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}
Referenced by multifunctional 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
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding comments through PyCHarm

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..
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unecessary stuff

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Explain what you're doing here @bsteubing

"""

backend = "functional_sqlite"
Expand Down
10 changes: 5 additions & 5 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 Down Expand Up @@ -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
55 changes: 44 additions & 11 deletions bw_functional/node_classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ def copy(self, *args, **kwargs):
MFActivity: A copy of the activity.
"""
act = super().copy(*args, **kwargs)
return self.__class__(**act)
return self.__class__(document=act._document)


class Process(MFActivity):
Expand Down Expand Up @@ -209,6 +209,33 @@ def save(self, signal: bool = True, data_already_set: bool = False, force_insert
if not created and old.data.get("allocation") != self.get("allocation"):
self.allocate()

def copy(self, *args, **kwargs):
"""
Create a copy of the process.

Args:
*args: Positional arguments for the copy operation.
**kwargs: Keyword arguments for the copy operation.

Returns:
Process: A copy of the process.
"""
act = super().copy(*args, **kwargs)

for function in self.functions():
input_database, input_code = function.key
output_database, output_code = act.key

MFExchange.ORMDataset.get(
input_database=input_database, input_code=input_code, output_database=output_database,
output_code=output_code, type="production").delete_instance()

copied_fn = function.copy(processor=act.key, signal=False)
copied_fn.create_processing_edge()
copied_fn.save()

return act

def deduct_type(self) -> str:
"""
Deduce the type of the process.
Expand All @@ -231,13 +258,13 @@ def new_product(self, **kwargs):
**kwargs: Additional arguments for creating the product.

Returns:
Function: A new product function.
Product: A new product function.
"""
kwargs["type"] = "product"
kwargs["processor"] = self.key
kwargs["database"] = self["database"]
kwargs["properties"] = self.get("default_properties", {})
return Function(**kwargs)
return Product(**kwargs)

def new_waste(self, **kwargs):
"""
Expand All @@ -247,13 +274,13 @@ def new_waste(self, **kwargs):
**kwargs: Additional arguments for creating the reduction.

Returns:
Function: A new waste function.
Product: A new waste function.
"""
kwargs["type"] = "waste"
kwargs["processor"] = self.key
kwargs["database"] = self["database"]
kwargs["properties"] = self.get("default_properties", {})
return Function(**kwargs)
return Product(**kwargs)

def new_default_property(self, name: str, unit: str, amount=1.0, normalize=False):
"""
Expand Down Expand Up @@ -350,7 +377,7 @@ def allocate(self, strategy_label: Optional[str] = None) -> Union[None, NoAlloca
alloc_function(self)


class Function(MFActivity):
class Product(MFActivity):
"""
Represents a function that can be either a 'product' or 'waste'.

Expand Down Expand Up @@ -411,8 +438,7 @@ def save(self, signal: bool = True, data_already_set: bool = False, force_insert

# If the function is new and there's no production exchange yet, create one
if created and not edge:
amount = 1.0 if self["type"] == "product" else -1.0
MFExchange(input=self.key, output=self["processor"], amount=amount, type="production").save()
self.create_processing_edge()

# If the function is new and has a processing edge, allocate the processor
if created and edge and isinstance(edge.output, Process):
Expand Down Expand Up @@ -462,6 +488,13 @@ def processing_edge(self) -> MFExchange | None:
return None
return list(excs)[0]

def create_processing_edge(self):
"""
Create a new processing edge for the function.
"""
amount = 1.0 if self["type"] == "product" else -1.0
MFExchange(input=self.key, output=self["processor"], amount=amount, type="production").save()

@property
def processor(self) -> Process | None:
"""
Expand Down Expand Up @@ -536,7 +569,7 @@ def valid(self, why=False):
"""
Validate the function.

A `Function` is considered valid if:
A `Product` is considered valid if:
- It has a `processor` key that is a tuple and corresponds to an existing process node.
- It has a `type` field, which must be either "product" or "waste".
- It passes the validation checks of the parent `MFActivity` class.
Expand Down Expand Up @@ -564,8 +597,8 @@ def valid(self, why=False):

if not self.get("type"):
errors.append("Missing field ``type``, function most be ``product`` or ``waste``")
elif self["type"] != "product" and self["type"] != "waste":
errors.append("Function ``type`` most be ``product`` or ``waste``")
elif self["type"] not in ["product", "waste", "orphaned_product"]:
errors.append("Product ``type`` most be ``product`` or ``waste``")

if errors:
if why:
Expand Down
4 changes: 2 additions & 2 deletions bw_functional/node_dispatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
from bw2data.backends.proxies import Activity
from bw2data.backends.schema import ActivityDataset

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


def functional_node_dispatcher(node_obj: Optional[ActivityDataset] = None) -> Activity:
"""Dispatch the correct node class depending on node_obj attributes."""
if node_obj and node_obj.type in ["product", "waste"]:
return Function(document=node_obj)
return Product(document=node_obj)
else:
return Process(document=node_obj)
8 changes: 4 additions & 4 deletions tests/test_allocation.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@

from bw_functional import FunctionalSQLiteDatabase
from bw_functional.allocation import generic_allocation
from bw_functional.node_classes import Process, Function, ReadOnlyProcess
from bw_functional.node_classes import Process, Product, ReadOnlyProcess


def check_basic_allocation_results(factor_1, factor_2, database):
nodes = sorted(database, key=lambda x: (x["name"], x.get("reference product", "")))
functions = list(filter(lambda x: isinstance(x, Function), nodes))
functions = list(filter(lambda x: isinstance(x, Product), nodes))
allocated = list(filter(lambda x: isinstance(x, ReadOnlyProcess), nodes))

# === Checking allocated process 1 ===
Expand Down Expand Up @@ -105,7 +105,7 @@ def test_without_allocation(basic):
for key, value in expected.items():
assert nodes[1][key] == value

assert isinstance(nodes[2], Function)
assert isinstance(nodes[2], Product)
assert not nodes[2].multifunctional
assert nodes[2].processor.key == nodes[1].key
expected = {
Expand All @@ -116,7 +116,7 @@ def test_without_allocation(basic):
for key, value in expected.items():
assert nodes[2][key] == value

assert isinstance(nodes[3], Function)
assert isinstance(nodes[3], Product)
assert not nodes[3].multifunctional
assert nodes[3].processor.key == nodes[1].key
expected = {
Expand Down
4 changes: 2 additions & 2 deletions tests/test_allocation_before_write.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from bw2data.tests import bw2test

import bw_functional as mf
from bw_functional.node_classes import Process, Function, ReadOnlyProcess
from bw_functional.node_classes import Process, Product, ReadOnlyProcess


@pytest.fixture
Expand All @@ -18,7 +18,7 @@ def allocate_then_write(basic_data):

def check_basic_allocation_results(factor_1, factor_2, database):
nodes = sorted(database, key=lambda x: (x["name"], x.get("reference product", "")))
functions = list(filter(lambda x: isinstance(x, Function), nodes))
functions = list(filter(lambda x: isinstance(x, Product), nodes))
allocated = list(filter(lambda x: isinstance(x, ReadOnlyProcess), nodes))

# === Checking allocated process 1 ===
Expand Down
2 changes: 1 addition & 1 deletion tests/test_readonly_process_creation_deletion.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

from bw_functional import FunctionalSQLiteDatabase
from bw_functional.allocation import generic_allocation
from bw_functional.node_classes import Process, Function
from bw_functional.node_classes import Process, Product


def test_allocation_creates_readonly_nodes(basic):
Expand Down