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):
return
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(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("related_obj").should_be_called().will_return("related_object")
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")
nest.disable_editing("related_obj").should_be_called()
collector.assign("relating_obj").should_be_called()
@@ -67,11 +67,15 @@ ARGUMENTS_DEPRECATION = {
new_argument="products",
replace_usecase="spatial.unassign_container",
),
"nest.assign_object": partial(
batching_argument_deprecation, prev_argument="related_object", new_argument="related_objects"
),
}
CACHED_USECASE_CLASSES = dict()
def run(
usecase_path: str,
ifc_file: Optional[ifcopenshell.file] = None,
@@ -68,6 +68,6 @@ class Usecase:
)
elif self.settings["cost_item"]:
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
@@ -82,7 +82,7 @@ class Usecase:
rel = ifcopenshell.api.run(
"nest.assign_object",
self.file,
related_object=cost_item,
related_objects=[cost_item],
relating_object=to_element,
)
# elif inverse.is_a("IfcRelAssignsToProduct"):
@@ -104,4 +104,4 @@ class Usecase:
elif isinstance(value, (tuple, list)) and from_element in value:
new_value = list(value)
new_value.append(to_element)
inverse[i] = new_value
inverse[i] = new_value
@@ -19,11 +19,17 @@
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.util.element
from typing import Union
class Usecase:
def __init__(self, file, related_object=None, relating_object=None):
"""Assigns an object as a nested child to a parent host
def __init__(
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
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.sequence.add_task.
:param related_object: The child of the nesting relationship, typically
an IfcElement.
:type related_object: ifcopenshell.entity_instance.entity_instance
:param related_objects: The list of children of the nesting relationship,
typically IfcElements.
:type related_objects: list[ifcopenshell.entity_instance.entity_instance]
:param relating_object: The host parent of the nesting relationship,
typically an IfcElement.
:type relating_object: ifcopenshell.entity_instance.entity_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:
@@ -90,30 +97,50 @@ class Usecase:
ifc_class="IfcSanitaryTerminal", predefined_type="SINK")
faucet = ifcopenshell.api.run("root.create_entity", model,
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.settings = {"related_object": related_object, "relating_object": relating_object}
self.settings = {"related_objects": related_objects, "relating_object": relating_object}
def execute(self):
nests = None
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:
def execute(self) -> Union[ifcopenshell.entity_instance, None]:
if not self.settings["related_objects"]:
return
if nests:
related_objects = list(nests.RelatedObjects)
related_objects.remove(self.settings["related_object"])
related_objects = set(self.settings["related_objects"])
relating_object = self.settings["relating_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:
nests.RelatedObjects = related_objects
nests.RelatedObjects = list(related_objects)
ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": nests})
else:
history = nests.OwnerHistory
@@ -121,10 +148,9 @@ class Usecase:
if history:
ifcopenshell.util.element.remove_deep2(self.file, history)
# assign elements to a new nesting
if is_nested_by:
related_objects = list(is_nested_by.RelatedObjects)
related_objects.append(self.settings["related_object"])
is_nested_by.RelatedObjects = related_objects
is_nested_by.RelatedObjects = list(set(is_nested_by.RelatedObjects) | related_objects)
ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": is_nested_by})
else:
is_nested_by = self.file.create_entity(
@@ -132,8 +158,12 @@ class Usecase:
**{
"GlobalId": ifcopenshell.guid.new(),
"OwnerHistory": ifcopenshell.api.run("owner.create_owner_history", self.file),
"RelatedObjects": [self.settings["related_object"]],
"RelatingObject": self.settings["relating_object"],
"RelatedObjects": list(related_objects),
"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
@@ -44,6 +44,6 @@ class Usecase:
ifcopenshell.api.run(
"nest.assign_object",
self.file,
related_object=self.settings["item"],
related_objects=[self.settings["item"]],
relating_object=self.settings["new_parent"],
)
@@ -42,8 +42,8 @@ class Usecase:
task = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcSite")
subtask1 = 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_object=subtask2, relating_object=task)
ifcopenshell.api.run("nest.assign_object", model, related_objects=[subtask1], 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
rel = ifcopenshell.api.run("nest.unassign_object", model, related_object=subtask1)
# Nothing is returned, as the relationship has no related objects
@@ -97,7 +97,7 @@ class Usecase:
ifcopenshell.api.run(
"nest.assign_object",
self.file,
related_object=resource,
related_objects=[resource],
relating_object=self.settings["parent_resource"],
)
else:
@@ -176,7 +176,7 @@ class Usecase:
rel = ifcopenshell.api.run(
"nest.assign_object",
self.file,
related_object=task,
related_objects=[task],
relating_object=self.settings["parent_task"],
)
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):
element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
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))
ifcopenshell.api.run("root.remove_product", self.file, product=element)
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):
element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
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))
ifcopenshell.api.run("root.remove_product", self.file, product=subelement)
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.remove_container", self.file, product=subelement)
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()
element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcFurniture")
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")
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)
@@ -1534,7 +1534,7 @@ class TestPartOf:
ifc = ifcopenshell.file()
element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcFurniture")
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")
run("The nest entity must match exactly 1/2", facet=facet, inst=subelement, expected=False)
facet = PartOf(relation="IFCRELNESTS", name="IFCFURNITURE")
@@ -1551,8 +1551,8 @@ class TestPartOf:
element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcFurniture")
subelement = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcDiscreteAccessory")
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_object=subsubelement, relating_object=subelement)
ifcopenshell.api.run("nest.assign_object", ifc, related_objects=[subelement], relating_object=element)
ifcopenshell.api.run("nest.assign_object", ifc, related_objects=[subsubelement], relating_object=subelement)
facet = PartOf(relation="IFCRELNESTS", name="IFCFURNITURE")
run("Nesting may be indirect", facet=facet, inst=subsubelement, expected=True)