nest.assign_object - support batching #4474

This commit is contained in:
Andrej730
2024-04-09 16:41:34 +05:00
parent d1bcec7a6a
commit 97db95085a
15 changed files with 173 additions and 45 deletions
+3 -1
View File
@@ -29,7 +29,9 @@ def assign_object(ifc, nest, collector, relating_obj=None, related_obj=None):
if not nest.can_nest(relating_obj, related_obj): if not nest.can_nest(relating_obj, related_obj):
return return
rel = ifc.run( rel = ifc.run(
"nest.assign_object", related_object=ifc.get_entity(related_obj), relating_object=ifc.get_entity(relating_obj) "nest.assign_object",
related_objects=[ifc.get_entity(related_obj)],
relating_object=ifc.get_entity(relating_obj),
) )
collector.assign(relating_obj) collector.assign(relating_obj)
collector.assign(related_obj) collector.assign(related_obj)
+1 -1
View File
@@ -38,7 +38,7 @@ class TestAssignObject:
ifc.get_entity("relating_obj").should_be_called().will_return("relating_object") ifc.get_entity("relating_obj").should_be_called().will_return("relating_object")
ifc.get_entity("related_obj").should_be_called().will_return("related_object") ifc.get_entity("related_obj").should_be_called().will_return("related_object")
ifc.run( ifc.run(
"nest.assign_object", related_object="related_object", relating_object="relating_object" "nest.assign_object", related_objects=["related_object"], relating_object="relating_object"
).should_be_called().will_return("rel") ).should_be_called().will_return("rel")
nest.disable_editing("related_obj").should_be_called() nest.disable_editing("related_obj").should_be_called()
collector.assign("relating_obj").should_be_called() collector.assign("relating_obj").should_be_called()
@@ -67,11 +67,15 @@ ARGUMENTS_DEPRECATION = {
new_argument="products", new_argument="products",
replace_usecase="spatial.unassign_container", replace_usecase="spatial.unassign_container",
), ),
"nest.assign_object": partial(
batching_argument_deprecation, prev_argument="related_object", new_argument="related_objects"
),
} }
CACHED_USECASE_CLASSES = dict() CACHED_USECASE_CLASSES = dict()
def run( def run(
usecase_path: str, usecase_path: str,
ifc_file: Optional[ifcopenshell.file] = None, ifc_file: Optional[ifcopenshell.file] = None,
@@ -68,6 +68,6 @@ class Usecase:
) )
elif self.settings["cost_item"]: elif self.settings["cost_item"]:
ifcopenshell.api.run( ifcopenshell.api.run(
"nest.assign_object", self.file, related_object=cost_item, relating_object=self.settings["cost_item"] "nest.assign_object", self.file, related_objects=[cost_item], relating_object=self.settings["cost_item"]
) )
return cost_item return cost_item
@@ -82,7 +82,7 @@ class Usecase:
rel = ifcopenshell.api.run( rel = ifcopenshell.api.run(
"nest.assign_object", "nest.assign_object",
self.file, self.file,
related_object=cost_item, related_objects=[cost_item],
relating_object=to_element, relating_object=to_element,
) )
# elif inverse.is_a("IfcRelAssignsToProduct"): # elif inverse.is_a("IfcRelAssignsToProduct"):
@@ -104,4 +104,4 @@ class Usecase:
elif isinstance(value, (tuple, list)) and from_element in value: elif isinstance(value, (tuple, list)) and from_element in value:
new_value = list(value) new_value = list(value)
new_value.append(to_element) new_value.append(to_element)
inverse[i] = new_value inverse[i] = new_value
@@ -19,11 +19,17 @@
import ifcopenshell import ifcopenshell
import ifcopenshell.api import ifcopenshell.api
import ifcopenshell.util.element import ifcopenshell.util.element
from typing import Union
class Usecase: class Usecase:
def __init__(self, file, related_object=None, relating_object=None): def __init__(
"""Assigns an object as a nested child to a parent host self,
file: ifcopenshell.file,
related_objects: list[ifcopenshell.entity_instance],
relating_object: ifcopenshell.entity_instance,
):
"""Assigns objects as nested children to a parent host
All physical IFC model elements must be part of a hierarchical tree All physical IFC model elements must be part of a hierarchical tree
called the "spatial decomposition", where large things are made up of called the "spatial decomposition", where large things are made up of
@@ -72,14 +78,15 @@ class Usecase:
ifcopenshell.api.cost.add_cost_item or ifcopenshell.api.cost.add_cost_item or
ifcopenshell.api.sequence.add_task. ifcopenshell.api.sequence.add_task.
:param related_object: The child of the nesting relationship, typically :param related_objects: The list of children of the nesting relationship,
an IfcElement. typically IfcElements.
:type related_object: ifcopenshell.entity_instance.entity_instance :type related_objects: list[ifcopenshell.entity_instance.entity_instance]
:param relating_object: The host parent of the nesting relationship, :param relating_object: The host parent of the nesting relationship,
typically an IfcElement. typically an IfcElement.
:type relating_object: ifcopenshell.entity_instance.entity_instance :type relating_object: ifcopenshell.entity_instance.entity_instance
:return: The IfcRelNests relationship instance :return: The IfcRelNests relationship instance
:rtype: ifcopenshell.entity_instance.entity_instance or `None` if `related_objects` was empty list.
:rtype: Union[ifcopenshell.entity_instance.entity_instance, None]
Example: Example:
@@ -90,30 +97,50 @@ class Usecase:
ifc_class="IfcSanitaryTerminal", predefined_type="SINK") ifc_class="IfcSanitaryTerminal", predefined_type="SINK")
faucet = ifcopenshell.api.run("root.create_entity", model, faucet = ifcopenshell.api.run("root.create_entity", model,
ifc_class="IfcValve", predefined_type="FAUCET") ifc_class="IfcValve", predefined_type="FAUCET")
ifcopenshell.api.run("nest.assign_object", model, related_object=faucet, relating_object=sink) ifcopenshell.api.run("nest.assign_object", model, related_objects=[faucet], relating_object=sink)
""" """
self.file = file self.file = file
self.settings = {"related_object": related_object, "relating_object": relating_object} self.settings = {"related_objects": related_objects, "relating_object": relating_object}
def execute(self): def execute(self) -> Union[ifcopenshell.entity_instance, None]:
nests = None if not self.settings["related_objects"]:
if self.settings["related_object"].Nests:
nests = self.settings["related_object"].Nests[0]
is_nested_by = None
for rel in self.settings["relating_object"].IsNestedBy:
if rel.is_a("IfcRelNests"):
is_nested_by = rel
break
if nests and nests == is_nested_by:
return return
if nests: related_objects = set(self.settings["related_objects"])
related_objects = list(nests.RelatedObjects) relating_object = self.settings["relating_object"]
related_objects.remove(self.settings["related_object"]) is_nested_by = next((i for i in relating_object.IsNestedBy), None)
previous_nests_rels: set[ifcopenshell.entity_instance] = set()
objects_without_nests: list[ifcopenshell.entity_instance] = []
objects_with_nests: list[ifcopenshell.entity_instance] = []
# check if there is anything to change
for object in related_objects:
object_rel = next(iter(object.Nests), None)
if object_rel is None:
objects_without_nests.append(object)
continue
# either is_nested_by is None or product is part of different rel
if object_rel != is_nested_by:
previous_nests_rels.add(object_rel)
objects_with_nests.append(object)
# products with already assigned nestings will be skipped
objects_to_change = objects_without_nests + objects_with_nests
# nothing to change
if not objects_to_change:
return is_nested_by
# NOTE: An object can both be nested and assigned to a container or aggregate.
# unassign elements from previous nests
for nests in previous_nests_rels:
related_objects = set(nests.RelatedObjects) - related_objects
if related_objects: if related_objects:
nests.RelatedObjects = related_objects nests.RelatedObjects = list(related_objects)
ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": nests}) ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": nests})
else: else:
history = nests.OwnerHistory history = nests.OwnerHistory
@@ -121,10 +148,9 @@ class Usecase:
if history: if history:
ifcopenshell.util.element.remove_deep2(self.file, history) ifcopenshell.util.element.remove_deep2(self.file, history)
# assign elements to a new nesting
if is_nested_by: if is_nested_by:
related_objects = list(is_nested_by.RelatedObjects) is_nested_by.RelatedObjects = list(set(is_nested_by.RelatedObjects) | related_objects)
related_objects.append(self.settings["related_object"])
is_nested_by.RelatedObjects = related_objects
ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": is_nested_by}) ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": is_nested_by})
else: else:
is_nested_by = self.file.create_entity( is_nested_by = self.file.create_entity(
@@ -132,8 +158,12 @@ class Usecase:
**{ **{
"GlobalId": ifcopenshell.guid.new(), "GlobalId": ifcopenshell.guid.new(),
"OwnerHistory": ifcopenshell.api.run("owner.create_owner_history", self.file), "OwnerHistory": ifcopenshell.api.run("owner.create_owner_history", self.file),
"RelatedObjects": [self.settings["related_object"]], "RelatedObjects": list(related_objects),
"RelatingObject": self.settings["relating_object"], "RelatingObject": relating_object,
} }
) )
# NOTE: Creating a nesting relationship doesn't localize the object's placement,
# unlike assigning it to an aggregate or a container.
return is_nested_by return is_nested_by
@@ -44,6 +44,6 @@ class Usecase:
ifcopenshell.api.run( ifcopenshell.api.run(
"nest.assign_object", "nest.assign_object",
self.file, self.file,
related_object=self.settings["item"], related_objects=[self.settings["item"]],
relating_object=self.settings["new_parent"], relating_object=self.settings["new_parent"],
) )
@@ -42,8 +42,8 @@ class Usecase:
task = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcSite") task = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcSite")
subtask1 = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcBuilding") subtask1 = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcBuilding")
subtask2 = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcBuilding") subtask2 = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcBuilding")
ifcopenshell.api.run("nest.assign_object", model, related_object=subtask1, relating_object=task) ifcopenshell.api.run("nest.assign_object", model, related_objects=[subtask1], relating_object=task)
ifcopenshell.api.run("nest.assign_object", model, related_object=subtask2, relating_object=task) ifcopenshell.api.run("nest.assign_object", model, related_objects=[subtask2], relating_object=task)
# The relationship is returned as task still has subtask2 # The relationship is returned as task still has subtask2
rel = ifcopenshell.api.run("nest.unassign_object", model, related_object=subtask1) rel = ifcopenshell.api.run("nest.unassign_object", model, related_object=subtask1)
# Nothing is returned, as the relationship has no related objects # Nothing is returned, as the relationship has no related objects
@@ -97,7 +97,7 @@ class Usecase:
ifcopenshell.api.run( ifcopenshell.api.run(
"nest.assign_object", "nest.assign_object",
self.file, self.file,
related_object=resource, related_objects=[resource],
relating_object=self.settings["parent_resource"], relating_object=self.settings["parent_resource"],
) )
else: else:
@@ -176,7 +176,7 @@ class Usecase:
rel = ifcopenshell.api.run( rel = ifcopenshell.api.run(
"nest.assign_object", "nest.assign_object",
self.file, self.file,
related_object=task, related_objects=[task],
relating_object=self.settings["parent_task"], relating_object=self.settings["parent_task"],
) )
if self.settings["parent_task"].Identification: if self.settings["parent_task"].Identification:
@@ -0,0 +1,17 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# 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 <http://www.gnu.org/licenses/>.
@@ -0,0 +1,67 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# 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 <http://www.gnu.org/licenses/>.
import pytest
import test.bootstrap
import ifcopenshell.api
import ifcopenshell.util.element
import ifcopenshell.util.placement
class TestAssignObject(test.bootstrap.IFC4):
def test_assigning_a_nesting(self):
element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcSanitaryTerminal")
subelement1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcValve")
subelement2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcValve")
rel = ifcopenshell.api.run(
"nest.assign_object", self.file, related_objects=[subelement1, subelement2], relating_object=element
)
assert ifcopenshell.util.element.get_nest(subelement1) == element
assert ifcopenshell.util.element.get_nest(subelement2) == element
assert rel.is_a("IfcRelNests")
def test_doing_nothing_if_the_nesting_is_already_assigned(self):
element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcSanitaryTerminal")
subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcValve")
ifcopenshell.api.run("nest.assign_object", self.file, related_objects=[subelement], relating_object=element)
total_elements = len([e for e in self.file])
ifcopenshell.api.run("nest.assign_object", self.file, related_objects=[subelement], relating_object=element)
assert len([e for e in self.file]) == total_elements
def test_that_old_nesting_relationships_are_updated_if_they_still_have_elements(self):
element1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcSanitaryTerminal")
element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcSanitaryTerminal")
subelement1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcValve")
subelement2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcValve")
ifcopenshell.api.run(
"nest.assign_object", self.file, related_objects=[subelement1, subelement2], relating_object=element1
)
rel = subelement1.Nests[0]
assert len(rel.RelatedObjects) == 2
ifcopenshell.api.run("nest.assign_object", self.file, related_objects=[subelement1], relating_object=element2)
assert len(rel.RelatedObjects) == 1
def test_that_old_nesting_relationships_are_purged_if_no_more_elements_are_nested(self):
element1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcSanitaryTerminal")
element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcSanitaryTerminal")
subelement1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcValve")
ifcopenshell.api.run("nest.assign_object", self.file, related_objects=[subelement1], relating_object=element1)
rel_id = subelement1.Nests[0].id()
ifcopenshell.api.run("nest.assign_object", self.file, related_objects=[subelement1], relating_object=element2)
with pytest.raises(RuntimeError):
self.file.by_id(rel_id)
@@ -190,7 +190,7 @@ class TestRemoveProduct(test.bootstrap.IFC4):
def test_removing_all_nesting_relationships_of_a_whole(self): def test_removing_all_nesting_relationships_of_a_whole(self):
element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBeam") subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBeam")
ifcopenshell.api.run("nest.assign_object", self.file, related_object=subelement, relating_object=element) ifcopenshell.api.run("nest.assign_object", self.file, related_objects=[subelement], relating_object=element)
total_entities = len(list(self.file)) total_entities = len(list(self.file))
ifcopenshell.api.run("root.remove_product", self.file, product=element) ifcopenshell.api.run("root.remove_product", self.file, product=element)
assert len(list(self.file)) == total_entities - 2 assert len(list(self.file)) == total_entities - 2
@@ -201,7 +201,7 @@ class TestRemoveProduct(test.bootstrap.IFC4):
def test_removing_all_nesting_relationships_of_a_part(self): def test_removing_all_nesting_relationships_of_a_part(self):
element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBeam") subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBeam")
ifcopenshell.api.run("nest.assign_object", self.file, related_object=subelement, relating_object=element) ifcopenshell.api.run("nest.assign_object", self.file, related_objects=[subelement], relating_object=element)
total_entities = len(list(self.file)) total_entities = len(list(self.file))
ifcopenshell.api.run("root.remove_product", self.file, product=subelement) ifcopenshell.api.run("root.remove_product", self.file, product=subelement)
assert len(list(self.file)) == total_entities - 2 assert len(list(self.file)) == total_entities - 2
@@ -107,3 +107,11 @@ class TestTemporarySupportForDeprecatedAPIArguments(test.bootstrap.IFC4):
ifcopenshell.api.run("spatial.assign_container", self.file, products=[subelement], relating_structure=element) ifcopenshell.api.run("spatial.assign_container", self.file, products=[subelement], relating_structure=element)
ifcopenshell.api.run("spatial.remove_container", self.file, product=subelement) ifcopenshell.api.run("spatial.remove_container", self.file, product=subelement)
assert ifcopenshell.util.element.get_container(subelement) is None assert ifcopenshell.util.element.get_container(subelement) is None
@deprecation_check
def test_assigning_a_nesting(self):
element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcSanitaryTerminal")
subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcValve")
rel = ifcopenshell.api.run("nest.assign_object", self.file, related_object=subelement, relating_object=element)
assert ifcopenshell.util.element.get_nest(subelement) == element
assert rel.is_a("IfcRelNests")
+4 -4
View File
@@ -1526,7 +1526,7 @@ class TestPartOf:
ifc = ifcopenshell.file() ifc = ifcopenshell.file()
element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcFurniture") element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcFurniture")
subelement = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcDiscreteAccessory") subelement = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcDiscreteAccessory")
ifcopenshell.api.run("nest.assign_object", ifc, related_object=subelement, relating_object=element) ifcopenshell.api.run("nest.assign_object", ifc, related_objects=[subelement], relating_object=element)
facet = PartOf(name="IFCFURNITURE", relation="IFCRELNESTS") facet = PartOf(name="IFCFURNITURE", relation="IFCRELNESTS")
run("Any nested part passes a nest relationship", facet=facet, inst=subelement, expected=True) run("Any nested part passes a nest relationship", facet=facet, inst=subelement, expected=True)
run("Any nested whole fails a nest relationship", facet=facet, inst=element, expected=False) run("Any nested whole fails a nest relationship", facet=facet, inst=element, expected=False)
@@ -1534,7 +1534,7 @@ class TestPartOf:
ifc = ifcopenshell.file() ifc = ifcopenshell.file()
element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcFurniture") element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcFurniture")
subelement = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcDiscreteAccessory") subelement = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcDiscreteAccessory")
ifcopenshell.api.run("nest.assign_object", ifc, related_object=subelement, relating_object=element) ifcopenshell.api.run("nest.assign_object", ifc, related_objects=[subelement], relating_object=element)
facet = PartOf(relation="IFCRELNESTS", name="IFCBEAM") facet = PartOf(relation="IFCRELNESTS", name="IFCBEAM")
run("The nest entity must match exactly 1/2", facet=facet, inst=subelement, expected=False) run("The nest entity must match exactly 1/2", facet=facet, inst=subelement, expected=False)
facet = PartOf(relation="IFCRELNESTS", name="IFCFURNITURE") facet = PartOf(relation="IFCRELNESTS", name="IFCFURNITURE")
@@ -1551,8 +1551,8 @@ class TestPartOf:
element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcFurniture") element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcFurniture")
subelement = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcDiscreteAccessory") subelement = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcDiscreteAccessory")
subsubelement = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcMechanicalFastener") subsubelement = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcMechanicalFastener")
ifcopenshell.api.run("nest.assign_object", ifc, related_object=subelement, relating_object=element) ifcopenshell.api.run("nest.assign_object", ifc, related_objects=[subelement], relating_object=element)
ifcopenshell.api.run("nest.assign_object", ifc, related_object=subsubelement, relating_object=subelement) ifcopenshell.api.run("nest.assign_object", ifc, related_objects=[subsubelement], relating_object=subelement)
facet = PartOf(relation="IFCRELNESTS", name="IFCFURNITURE") facet = PartOf(relation="IFCRELNESTS", name="IFCFURNITURE")
run("Nesting may be indirect", facet=facet, inst=subsubelement, expected=True) run("Nesting may be indirect", facet=facet, inst=subsubelement, expected=True)