From 6b48e8b4b962d4ddf4c0f3a1266afad05fda8524 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 11 Apr 2024 15:54:06 +0500 Subject: [PATCH] system.assign_system - support batching #4474 --- src/blenderbim/blenderbim/core/system.py | 2 +- src/blenderbim/test/core/test_system.py | 2 +- src/blenderbim/test/tool/test_system.py | 2 +- .../ifcopenshell/api/__init__.py | 3 ++ .../ifcopenshell/api/group/assign_group.py | 9 +++- .../ifcopenshell/api/system/assign_system.py | 44 ++++++++---------- .../ifcopenshell/api/system/connect_port.py | 4 +- .../api/system/disconnect_port.py | 4 +- .../api/system/unassign_system.py | 2 +- .../test/api/group/test_assign_group.py | 20 +++++---- .../test/api/system/test_assign_system.py | 45 +++++++++++++++++++ src/ifcopenshell-python/test/api/test_api.py | 12 +++++ .../test/util/test_system.py | 18 ++++++-- 13 files changed, 119 insertions(+), 48 deletions(-) create mode 100644 src/ifcopenshell-python/test/api/system/test_assign_system.py diff --git a/src/blenderbim/blenderbim/core/system.py b/src/blenderbim/blenderbim/core/system.py index 3c6ce42e5a..1e5b026046 100644 --- a/src/blenderbim/blenderbim/core/system.py +++ b/src/blenderbim/blenderbim/core/system.py @@ -55,7 +55,7 @@ def disable_editing_system(system): def assign_system(ifc, system=None, product=None): - ifc.run("system.assign_system", product=product, system=system) + ifc.run("system.assign_system", products=[product], system=system) def unassign_system(ifc, system=None, product=None): diff --git a/src/blenderbim/test/core/test_system.py b/src/blenderbim/test/core/test_system.py index f77cae4cd1..82ef966b5c 100644 --- a/src/blenderbim/test/core/test_system.py +++ b/src/blenderbim/test/core/test_system.py @@ -74,7 +74,7 @@ class TestDisableEditingSystem: class TestAssignSystem: def test_run(self, ifc): - ifc.run("system.assign_system", product="product", system="system").should_be_called() + ifc.run("system.assign_system", products=["product"], system="system").should_be_called() subject.assign_system(ifc, system="system", product="product") diff --git a/src/blenderbim/test/tool/test_system.py b/src/blenderbim/test/tool/test_system.py index 419c46cc7f..93235545b1 100644 --- a/src/blenderbim/test/tool/test_system.py +++ b/src/blenderbim/test/tool/test_system.py @@ -262,7 +262,7 @@ class TestSelectSystemProducts(NewFile): tool.Ifc().set(ifc) element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcPump") system = ifcopenshell.api.run("system.add_system", ifc, ifc_class="IfcSystem") - ifcopenshell.api.run("system.assign_system", ifc, product=element, system=system) + ifcopenshell.api.run("system.assign_system", ifc, products=[element], system=system) obj = bpy.data.objects.new("Object", None) bpy.context.scene.collection.objects.link(obj) tool.Ifc.link(element, obj) diff --git a/src/ifcopenshell-python/ifcopenshell/api/__init__.py b/src/ifcopenshell-python/ifcopenshell/api/__init__.py index 04fdfde724..10ade30ab8 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/__init__.py +++ b/src/ifcopenshell-python/ifcopenshell/api/__init__.py @@ -79,6 +79,9 @@ ARGUMENTS_DEPRECATION = { "type.unassign_type": partial( batching_argument_deprecation, prev_argument="related_object", new_argument="related_objects" ), + "system.assign_system": partial( + batching_argument_deprecation, prev_argument="product", new_argument="products" + ), } diff --git a/src/ifcopenshell-python/ifcopenshell/api/group/assign_group.py b/src/ifcopenshell-python/ifcopenshell/api/group/assign_group.py index 3a212c33c7..78d787f741 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/group/assign_group.py +++ b/src/ifcopenshell-python/ifcopenshell/api/group/assign_group.py @@ -18,6 +18,7 @@ import ifcopenshell import ifcopenshell.api +from typing import Union class Usecase: @@ -34,7 +35,8 @@ class Usecase: :param group: The IfcGroup to assign the products to :type group: ifcopenshell.entity_instance.entity_instance :return: The IfcRelAssignsToGroup relationship - :rtype: ifcopenshell.entity_instance.entity_instance + or `None` if `products` was empty list. + :rtype: Union[ifcopenshell.entity_instance.entity_instance, None] Example: @@ -50,7 +52,10 @@ class Usecase: "group": group, } - def execute(self) -> ifcopenshell.entity_instance: + def execute(self) -> Union[ifcopenshell.entity_instance, None]: + if not self.settings["products"]: + return + if not self.settings["group"].IsGroupedBy: return self.file.create_entity( "IfcRelAssignsToGroup", diff --git a/src/ifcopenshell-python/ifcopenshell/api/system/assign_system.py b/src/ifcopenshell-python/ifcopenshell/api/system/assign_system.py index d21e2b88e0..3df49d776d 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/system/assign_system.py +++ b/src/ifcopenshell-python/ifcopenshell/api/system/assign_system.py @@ -22,17 +22,23 @@ import ifcopenshell.util.system class Usecase: - def __init__(self, file, product=None, system=None): - """Assigns a distribution element to a system + def __init__( + self, + file: ifcopenshell.file, + products: list[ifcopenshell.entity_instance], + system: ifcopenshell.entity_instance, + ): + """Assigns distribution elements to a system Note that it is not necessary to assign distribution ports to a system. - :param product: The IfcDistributionElement to assign to the system. - :type product: ifcopenshell.entity_instance.entity_instance + :param products: The list of IfcDistributionElements to assign to the system. + :type products: list[ifcopenshell.entity_instance.entity_instance] :param system: The IfcSystem you want to assign the element to. :type system: ifcopenshell.entity_instance.entity_instance :return: The IfcRelAssignsToGroup relationship - :rtype: ifcopenshell.entity_instance.entity_instance + or `None` if `products` was empty list. + :rtype: [ifcopenshell.entity_instance.entity_instance, None] Example: @@ -46,32 +52,20 @@ class Usecase: ifc_class="IfcDuctSegment", predefined_type="RIGIDSEGMENT") # This duct is part of the system - ifcopenshell.api.run("system.assign_system", model, product=duct, system=system) + ifcopenshell.api.run("system.assign_system", model, products=[duct], system=system) """ self.file = file self.settings = { - "product": product, + "products": products, "system": system, } def execute(self): system = self.settings["system"] - product = self.settings["product"] - if not ifcopenshell.util.system.is_assignable(product, system): - raise TypeError(f"You cannot assign an {product.is_a()} to an {system.is_a()}") + products = self.settings["products"] - if not self.settings["system"].IsGroupedBy: - return self.file.create_entity( - "IfcRelAssignsToGroup", - **{ - "GlobalId": ifcopenshell.guid.new(), - "OwnerHistory": ifcopenshell.api.run("owner.create_owner_history", self.file), - "RelatedObjects": [self.settings["product"]], - "RelatingGroup": self.settings["system"], - }, - ) - rel = self.settings["system"].IsGroupedBy[0] - related_objects = set(rel.RelatedObjects) or set() - related_objects.add(self.settings["product"]) - rel.RelatedObjects = list(related_objects) - ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": rel}) + if not all(ifcopenshell.util.system.is_assignable(failed_product := product, system) for product in products): + raise TypeError(f"You cannot assign an {failed_product.is_a()} to an {system.is_a()}") + + rel = ifcopenshell.api.run("group.assign_group", self.file, products=products, group=system) + return rel diff --git a/src/ifcopenshell-python/ifcopenshell/api/system/connect_port.py b/src/ifcopenshell-python/ifcopenshell/api/system/connect_port.py index c919a153d0..4d0b75e8a6 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/system/connect_port.py +++ b/src/ifcopenshell-python/ifcopenshell/api/system/connect_port.py @@ -77,8 +77,8 @@ class Usecase: ifc_class="IfcDuctFitting", predefined_type="BEND") # The duct and fitting is part of the system - ifcopenshell.api.run("system.assign_system", model, product=duct, system=system) - ifcopenshell.api.run("system.assign_system", model, product=fitting, system=system) + ifcopenshell.api.run("system.assign_system", model, products=[duct], system=system) + ifcopenshell.api.run("system.assign_system", model, products=[fitting], system=system) # Create 2 ports, one for either end of both the duct and fitting. duct_port1 = ifcopenshell.api.run("system.add_port", model, element=duct) diff --git a/src/ifcopenshell-python/ifcopenshell/api/system/disconnect_port.py b/src/ifcopenshell-python/ifcopenshell/api/system/disconnect_port.py index dd17078964..12d48af4b2 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/system/disconnect_port.py +++ b/src/ifcopenshell-python/ifcopenshell/api/system/disconnect_port.py @@ -47,8 +47,8 @@ class Usecase: ifc_class="IfcDuctFitting", predefined_type="BEND") # The duct and fitting is part of the system - ifcopenshell.api.run("system.assign_system", model, product=duct, system=system) - ifcopenshell.api.run("system.assign_system", model, product=fitting, system=system) + ifcopenshell.api.run("system.assign_system", model, products=[duct], system=system) + ifcopenshell.api.run("system.assign_system", model, products=[fitting], system=system) # Create 2 ports, one for either end of both the duct and fitting. duct_port1 = ifcopenshell.api.run("system.add_port", model, element=duct) diff --git a/src/ifcopenshell-python/ifcopenshell/api/system/unassign_system.py b/src/ifcopenshell-python/ifcopenshell/api/system/unassign_system.py index c96ffa02a1..10383ffbbb 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/system/unassign_system.py +++ b/src/ifcopenshell-python/ifcopenshell/api/system/unassign_system.py @@ -44,7 +44,7 @@ class Usecase: ifc_class="IfcDuctSegment", predefined_type="RIGIDSEGMENT") # This duct is part of the system - ifcopenshell.api.run("system.assign_system", model, product=duct, system=system) + ifcopenshell.api.run("system.assign_system", model, products=[duct], system=system) # Not anymore! ifcopenshell.api.run("system.unassign_system", model, product=duct, system=system) diff --git a/src/ifcopenshell-python/test/api/group/test_assign_group.py b/src/ifcopenshell-python/test/api/group/test_assign_group.py index 88870b4c15..434dab2b32 100644 --- a/src/ifcopenshell-python/test/api/group/test_assign_group.py +++ b/src/ifcopenshell-python/test/api/group/test_assign_group.py @@ -18,28 +18,30 @@ import test.bootstrap import ifcopenshell.api +import ifcopenshell.util.element class TestAssignGroup(test.bootstrap.IFC4): def test_assign_group_for_multiple_elements(self): - element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcPump") - element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcPump") + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") group = ifcopenshell.api.run("group.add_group", self.file) # assign group for multiple elements ifcopenshell.api.run("group.assign_group", self.file, products=[element, element2], group=group) - assert len(rels := self.file.by_type("IfcRelAssignsToGroup")) == 1 - rel = rels[0] - assert rel.RelatingGroup == group - assert rel.RelatedObjects == (element, element2) + assert len(self.file.by_type("IfcRelAssignsToGroup")) == 1 + assert set(ifcopenshell.util.element.get_grouped_by(group)) == set(self.file.by_type("IfcWall")) def test_reuse_existing_relationship(self): self.test_assign_group_for_multiple_elements() rel = self.file.by_type("IfcRelAssignsToGroup")[0] - element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcPump") + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") group = self.file.by_type("IfcGroup")[0] ifcopenshell.api.run("group.assign_group", self.file, products=[element], group=group) assert len(self.file.by_type("IfcRelAssignsToGroup")) == 1 - assert rel.RelatingGroup == group - assert set(rel.RelatedObjects) == set(self.file.by_type("IfcPump")) + assert set(ifcopenshell.util.element.get_grouped_by(group)) == set(self.file.by_type("IfcWall")) + + +class TestAssignGroupIFC2X3(test.bootstrap.IFC2X3, TestAssignGroup): + pass diff --git a/src/ifcopenshell-python/test/api/system/test_assign_system.py b/src/ifcopenshell-python/test/api/system/test_assign_system.py new file mode 100644 index 0000000000..0026866835 --- /dev/null +++ b/src/ifcopenshell-python/test/api/system/test_assign_system.py @@ -0,0 +1,45 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2022 Dion Moult +# +# This file is part of IfcOpenShell. +# +# IfcOpenShell is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# IfcOpenShell is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with IfcOpenShell. If not, see . + +import pytest +import test.bootstrap +import ifcopenshell.api +import ifcopenshell.util.system + + +class TestAssignSystem(test.bootstrap.IFC4): + def test_assign_system(self): + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcFlowSegment") + element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcFlowSegment") + system = ifcopenshell.api.run("system.add_system", self.file) + ifcopenshell.api.run("system.assign_system", self.file, products=[element, element2], system=system) + assert len(self.file.by_type("IfcRelAssignsToGroup")) == 1 + assert set(ifcopenshell.util.system.get_system_elements(system)) == set(self.file.by_type("IfcFlowSegment")) + + def test_exception_on_unassignable_elements(self): + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcFlowSegment") + proj = self.file.createIfcProject() + system = ifcopenshell.api.run("system.add_system", self.file) + with pytest.raises(TypeError): + ifcopenshell.api.run("system.assign_system", self.file, products=[element, proj], system=system) + with pytest.raises(TypeError): + ifcopenshell.api.run("system.assign_system", self.file, products=[element], system=proj) + + +class TestAssignSystemIFC2X3(test.bootstrap.IFC2X3, TestAssignSystem): + pass diff --git a/src/ifcopenshell-python/test/api/test_api.py b/src/ifcopenshell-python/test/api/test_api.py index 8a33238806..458691cc56 100644 --- a/src/ifcopenshell-python/test/api/test_api.py +++ b/src/ifcopenshell-python/test/api/test_api.py @@ -139,3 +139,15 @@ class TestTemporarySupportForDeprecatedAPIArguments(test.bootstrap.IFC4): ifcopenshell.api.run("type.assign_type", self.file, related_objects=[element], relating_type=element_type) ifcopenshell.api.run("type.unassign_type", self.file, related_object=element) assert ifcopenshell.util.element.get_type(element) is None + + @deprecation_check + def test_assign_system(self): + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcPump") + system = ifcopenshell.api.run("system.add_system", self.file) + + # assign group for multiple elements + ifcopenshell.api.run("system.assign_system", self.file, product=element, system=system) + assert len(rels := self.file.by_type("IfcRelAssignsToGroup")) == 1 + rel = rels[0] + assert rel.RelatingGroup == system + assert rel.RelatedObjects == (element,) diff --git a/src/ifcopenshell-python/test/util/test_system.py b/src/ifcopenshell-python/test/util/test_system.py index 100b10fd9c..3035f07da6 100644 --- a/src/ifcopenshell-python/test/util/test_system.py +++ b/src/ifcopenshell-python/test/util/test_system.py @@ -22,11 +22,21 @@ import ifcopenshell.api import ifcopenshell.util.system as subject +class TestIsAssignable(test.bootstrap.IFC4): + def test_run(self): + project = self.file.createIfcProject() + system = self.file.createIfcSystem() + pump = self.file.createIfcPump() + assert subject.is_assignable(pump, system) + assert not subject.is_assignable(project, system) + assert not subject.is_assignable(pump, project) + + class TestGetSystemElements(test.bootstrap.IFC4): def test_run(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcPump") system = ifcopenshell.api.run("system.add_system", self.file, ifc_class="IfcSystem") - ifcopenshell.api.run("system.assign_system", self.file, product=element, system=system) + ifcopenshell.api.run("system.assign_system", self.file, products=[element], system=system) assert subject.get_system_elements(system) == [element] @@ -34,7 +44,7 @@ class TestGetElementSystems(test.bootstrap.IFC4): def test_run(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcPump") system = ifcopenshell.api.run("system.add_system", self.file, ifc_class="IfcSystem") - ifcopenshell.api.run("system.assign_system", self.file, product=element, system=system) + ifcopenshell.api.run("system.assign_system", self.file, products=[element], system=system) assert subject.get_element_systems(element) == [system] def test_do_not_get_non_services_groups(self): @@ -42,7 +52,7 @@ class TestGetElementSystems(test.bootstrap.IFC4): ifcopenshell.api.run( "system.assign_system", self.file, - product=element, + products=[element], system=self.file.createIfcGroup(), ) for not_assignable_system_class in ("IfcZone", "IfcStructuralAnalysisModel"): @@ -50,7 +60,7 @@ class TestGetElementSystems(test.bootstrap.IFC4): ifcopenshell.api.run( "system.assign_system", self.file, - product=element, + products=[element], system=self.file.create_entity(not_assignable_system_class), ) assert not subject.get_element_systems(element)