From 1450b9be59f0c3bc5c0058497687212764192199 Mon Sep 17 00:00:00 2001 From: Marin Visscher Date: Wed, 23 Apr 2025 12:48:51 +0200 Subject: [PATCH 1/3] Solved copying activities --- bw_functional/node_classes.py | 41 +++++++++++++++++++++++++++++++---- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/bw_functional/node_classes.py b/bw_functional/node_classes.py index 6712a1a..194e877 100644 --- a/bw_functional/node_classes.py +++ b/bw_functional/node_classes.py @@ -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): @@ -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. @@ -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): @@ -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: """ @@ -564,7 +597,7 @@ 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": + elif self["type"] not in ["product", "waste", "orphaned_product"]: errors.append("Function ``type`` most be ``product`` or ``waste``") if errors: From 8a1739b1d8df2cc0d9be5f3c9e41749591fe525a Mon Sep 17 00:00:00 2001 From: Marin Visscher Date: Fri, 25 Apr 2025 14:18:21 +0200 Subject: [PATCH 2/3] Check --- bw_functional/database.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bw_functional/database.py b/bw_functional/database.py index b69280a..a7d3386 100644 --- a/bw_functional/database.py +++ b/bw_functional/database.py @@ -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" From 84a80f84dfa2a4f7445faac88ee0371f882c4693 Mon Sep 17 00:00:00 2001 From: Marin Visscher Date: Mon, 23 Jun 2025 12:41:39 +0200 Subject: [PATCH 3/3] Refactor `Function` to `Product` --- README.md | 2 +- bw_functional/__init__.py | 4 ++-- bw_functional/allocation.py | 14 +++++++------- bw_functional/custom_allocation.py | 6 +++--- bw_functional/edge_classes.py | 10 +++++----- bw_functional/node_classes.py | 14 +++++++------- bw_functional/node_dispatch.py | 4 ++-- tests/test_allocation.py | 8 ++++---- tests/test_allocation_before_write.py | 4 ++-- tests/test_readonly_process_creation_deletion.py | 2 +- 10 files changed, 34 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 5ff89af..66dca89 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/bw_functional/__init__.py b/bw_functional/__init__.py index 50134e6..36f66e3 100644 --- a/bw_functional/__init__.py +++ b/bw_functional/__init__.py @@ -7,7 +7,7 @@ "generic_allocation", "list_available_properties", "Process", - "Function", + "Product", "MFExchange", "MFExchanges", "FunctionalSQLiteDatabase", @@ -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 diff --git a/bw_functional/allocation.py b/bw_functional/allocation.py index 8339fc6..482ee7c 100644 --- a/bw_functional/allocation.py +++ b/bw_functional/allocation.py @@ -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__) @@ -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: @@ -73,7 +73,7 @@ def generic_allocation( def get_property_value( - function: Function, + function: Product, property_label: str, ) -> float: """ @@ -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. @@ -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") diff --git a/bw_functional/custom_allocation.py b/bw_functional/custom_allocation.py index 0b6e6d2..d08408b 100644 --- a/bw_functional/custom_allocation.py +++ b/bw_functional/custom_allocation.py @@ -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) @@ -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) @@ -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: diff --git a/bw_functional/edge_classes.py b/bw_functional/edge_classes.py index 6ecd7fd..f6aebe5 100644 --- a/bw_functional/edge_classes.py +++ b/bw_functional/edge_classes.py @@ -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": @@ -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 @@ -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": @@ -150,7 +150,7 @@ 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) @@ -158,7 +158,7 @@ def delete(self, signal: bool = True): 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": diff --git a/bw_functional/node_classes.py b/bw_functional/node_classes.py index 194e877..d592c79 100644 --- a/bw_functional/node_classes.py +++ b/bw_functional/node_classes.py @@ -258,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): """ @@ -274,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): """ @@ -377,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'. @@ -569,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. @@ -598,7 +598,7 @@ def valid(self, why=False): if not self.get("type"): errors.append("Missing field ``type``, function most be ``product`` or ``waste``") elif self["type"] not in ["product", "waste", "orphaned_product"]: - errors.append("Function ``type`` most be ``product`` or ``waste``") + errors.append("Product ``type`` most be ``product`` or ``waste``") if errors: if why: diff --git a/bw_functional/node_dispatch.py b/bw_functional/node_dispatch.py index 2f3e1fb..60f4c54 100644 --- a/bw_functional/node_dispatch.py +++ b/bw_functional/node_dispatch.py @@ -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) diff --git a/tests/test_allocation.py b/tests/test_allocation.py index 3d44956..38bd4ff 100644 --- a/tests/test_allocation.py +++ b/tests/test_allocation.py @@ -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 === @@ -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 = { @@ -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 = { diff --git a/tests/test_allocation_before_write.py b/tests/test_allocation_before_write.py index d36f1cc..8f047ba 100644 --- a/tests/test_allocation_before_write.py +++ b/tests/test_allocation_before_write.py @@ -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 @@ -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 === diff --git a/tests/test_readonly_process_creation_deletion.py b/tests/test_readonly_process_creation_deletion.py index e7128da..df2eddc 100644 --- a/tests/test_readonly_process_creation_deletion.py +++ b/tests/test_readonly_process_creation_deletion.py @@ -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):