IfcDiff now uses the iterator to compare geometry, which seems significantly faster.

This commit is contained in:
Dion Moult
2022-10-24 13:06:03 +11:00
parent 226c94381b
commit 2f7643424d
+153 -34
View File
@@ -25,7 +25,9 @@ import json
import logging import logging
import argparse import argparse
import numpy as np import numpy as np
import multiprocessing
import ifcopenshell import ifcopenshell
import ifcopenshell.geom
import ifcopenshell.util.element import ifcopenshell.util.element
import ifcopenshell.util.placement import ifcopenshell.util.placement
import ifcopenshell.util.classification import ifcopenshell.util.classification
@@ -42,8 +44,8 @@ class IfcDiff:
:type old: ifcopenshell.file.file :type old: ifcopenshell.file.file
:param new: IFC file object for the old model :param new: IFC file object for the old model
:type new: ifcopenshell.file.file :type new: ifcopenshell.file.file
:param relationships: List of relationships to check. An empty list means :param relationships: List of relationships to check. None means that only
that only geometry and attributes are compared. geometry is compared.
:type relationships: list[string] :type relationships: list[string]
:param is_shallow: True if you want only the first difference to be listed. :param is_shallow: True if you want only the first difference to be listed.
False if you want all differences to be checked. Choosing False means False if you want all differences to be checked. Choosing False means
@@ -68,7 +70,7 @@ class IfcDiff:
self.new = new self.new = new
self.change_register = {} self.change_register = {}
self.representation_ids = {} self.representation_ids = {}
self.relationships = relationships self.relationships = relationships or ["geometry"]
self.precision = 1e-4 self.precision = 1e-4
self.is_shallow = is_shallow self.is_shallow = is_shallow
self.filter_elements = filter_elements self.filter_elements = filter_elements
@@ -83,32 +85,152 @@ class IfcDiff:
old_elements = set(e.GlobalId for e in selector.parse(self.old, self.filter_elements)) old_elements = set(e.GlobalId for e in selector.parse(self.old, self.filter_elements))
new_elements = set(e.GlobalId for e in selector.parse(self.new, self.filter_elements)) new_elements = set(e.GlobalId for e in selector.parse(self.new, self.filter_elements))
else: else:
old_elements = set(e.GlobalId for e in self.old.by_type("IfcProduct")) old_elements = self.old.by_type("IfcElement")
new_elements = set(e.GlobalId for e in self.new.by_type("IfcProduct")) if self.old.schema == "IFC2X3":
old_elements += self.old.by_type("IfcSpatialStructureElement")
else:
old_elements += self.old.by_type("IfcSpatialElement")
old_elements = set(e.GlobalId for e in old_elements if not e.is_a("IfcFeatureElement"))
new_elements = self.new.by_type("IfcElement")
if self.new.schema == "IFC2X3":
new_elements += self.new.by_type("IfcSpatialStructureElement")
else:
new_elements += self.new.by_type("IfcSpatialElement")
new_elements = set(e.GlobalId for e in new_elements if not e.is_a("IfcFeatureElement"))
print(" - {} item(s) are in the old model".format(len(old_elements)))
print(" - {} item(s) are in the new model".format(len(new_elements)))
self.deleted_elements = old_elements - new_elements self.deleted_elements = old_elements - new_elements
self.added_elements = new_elements - old_elements self.added_elements = new_elements - old_elements
same_elements = new_elements - self.added_elements same_elements = new_elements - self.added_elements
total_same_elements = len(same_elements) total_same_elements = len(same_elements)
print(" - {} item(s) were added".format(len(self.added_elements)))
print(" - {} item(s) were deleted".format(len(self.deleted_elements)))
print(" - {} item(s) are common to both models".format(total_same_elements))
total_diffed = 0 total_diffed = 0
potential_old_changes = []
potential_new_changes = []
should_check_attributes = False
should_check_geometry = False
should_check_other = False
for relationship in self.relationships:
if relationship == "attributes":
should_check_attributes = True
elif relationship == "geometry":
should_check_geometry = True
else:
should_check_other = True
for global_id in same_elements: for global_id in same_elements:
total_diffed += 1 total_diffed += 1
if total_diffed % 250 == 0: if total_diffed % 250 == 0:
print("{}/{} diffed ...".format(total_diffed, total_same_elements), end="\r", flush=True) print("{}/{} diffed ...".format(total_diffed, total_same_elements), end="\r", flush=True)
old = self.old.by_id(global_id) old = self.old.by_id(global_id)
new = self.new.by_id(global_id) new = self.new.by_id(global_id)
if self.diff_element(old, new) and self.is_shallow: if should_check_attributes:
continue if self.diff_element(old, new) and self.is_shallow:
if self.diff_element_relationships(old, new) and self.is_shallow: continue
continue if should_check_other:
diff = self.diff_element_geometry(old, new) if self.diff_element_relationships(old, new) and self.is_shallow:
if diff: continue
self.change_register.setdefault(new.GlobalId, {}).update({"geometry_changed": True}) if should_check_geometry:
# Option 1: check everything heuristically using the iterator (seems faster)
potential_old_changes.append(old)
potential_new_changes.append(new)
# Option 2: check first using Python, then fallback to iterator (twice as slow)
# diff = self.diff_element_basic_geometry(old, new)
# if diff:
# self.change_register.setdefault(new.GlobalId, {}).update({"geometry_changed": True})
# else:
# potential_old_changes.append(old)
# potential_new_changes.append(new)
print(" - {} item(s) had simple changes".format(len(self.change_register.keys())))
if potential_old_changes:
print(" - {} item(s) are queued for a detailed geometry check".format(len(potential_old_changes)))
print("... processing old shapes ...")
old_shapes = self.summarise_shapes(self.old, potential_old_changes)
print("... processing new shapes ...")
new_shapes = self.summarise_shapes(self.new, potential_new_changes)
print("... comparing shapes ...")
for global_id, old_shape in old_shapes.items():
new_shape = new_shapes.get(global_id, None)
if not new_shape:
self.change_register.setdefault(global_id, {}).update({"geometry_changed": True})
continue
del new_shapes[global_id]
diff = DeepDiff(old_shape, new_shape, math_epsilon=1e-5)
if diff:
self.change_register.setdefault(global_id, {}).update({"geometry_changed": True})
continue
for global_id in new_shapes.keys():
self.change_register.setdefault(global_id, {}).update({"geometry_changed": True})
print(" - {} item(s) were changed".format(len(self.change_register.keys())))
logging.disable(logging.NOTSET) logging.disable(logging.NOTSET)
def summarise_shapes(self, ifc, elements):
shapes = {}
iterator = ifcopenshell.geom.iterator(
self.get_settings(ifc), ifc, multiprocessing.cpu_count(), include=elements
)
valid_file = iterator.initialize()
while True:
shape = iterator.get()
element = ifc.by_id(shape.id)
geometry = shape.geometry
shapes[element.GlobalId] = {
"total_verts": len(geometry.verts),
"sum_verts": sum(geometry.verts),
"min_vert": min(geometry.verts),
"max_vert": max(geometry.verts),
"matrix": tuple(shape.transformation.matrix.data),
"openings": sorted(
[o.RelatedOpeningElement.GlobalId for o in getattr(element, "HasOpenings", []) or []]
),
"projections": sorted(
[o.RelatedFeatureElement.GlobalId for o in getattr(element, "HasProjections", []) or []]
),
}
if not iterator.next():
break
return shapes
def get_settings(self, ifc):
settings = ifcopenshell.geom.settings()
settings.set(settings.STRICT_TOLERANCE, True)
# Are you feeling lucky?
settings.set(settings.DISABLE_BOOLEAN_RESULT, True)
# Are you feeling very lucky?
settings.set(settings.DISABLE_OPENING_SUBTRACTIONS, True)
# Facetation is to accommodate broken Revit files
# See https://forums.buildingsmart.org/t/suggestions-on-how-to-improve-clarity-of-representation-context-usage-in-documentation/3663/6?u=moult
body_contexts = [
c.id()
for c in ifc.by_type("IfcGeometricRepresentationSubContext")
if c.ContextIdentifier in ["Body", "Facetation"]
]
# Ideally, all representations should be in a subcontext, but some BIM programs don't do this correctly
body_contexts.extend(
[
c.id()
for c in ifc.by_type("IfcGeometricRepresentationContext", include_subtypes=False)
if c.ContextType == "Model"
]
)
if body_contexts:
settings.set_context_ids(body_contexts)
return settings
def export(self, path): def export(self, path):
with open(path, "w", encoding="utf-8") as diff_file: with open(path, "w", encoding="utf-8") as diff_file:
json.dump( json.dump(
@@ -181,34 +303,35 @@ class IfcDiff:
self.change_register.setdefault(new.GlobalId, {}).update({"classification_changed": True}) self.change_register.setdefault(new.GlobalId, {}).update({"classification_changed": True})
return True return True
def diff_element_geometry(self, old, new): def diff_element_basic_geometry(self, old, new):
old_placement = ifcopenshell.util.placement.get_local_placement(old.ObjectPlacement) old_placement = ifcopenshell.util.placement.get_local_placement(old.ObjectPlacement)
new_placement = ifcopenshell.util.placement.get_local_placement(new.ObjectPlacement) new_placement = ifcopenshell.util.placement.get_local_placement(new.ObjectPlacement)
if not np.allclose(old_placement[:, 3], new_placement[:, 3], atol=self.precision): if not np.allclose(old_placement[:, 3], new_placement[:, 3], atol=self.precision):
return True return True
if not np.allclose(old_placement[0:3, 0:3], new_placement[0:3, 0:3], atol=1e-2): if not np.allclose(old_placement[0:3, 0:3], new_placement[0:3, 0:3], atol=1e-2):
return True return True
old_openings = [o.RelatedOpeningElement.GlobalId for o in getattr(old, "HasOpenings", []) or []] old_openings = sorted([o.RelatedOpeningElement.GlobalId for o in getattr(old, "HasOpenings", []) or []])
new_openings = [o.RelatedOpeningElement.GlobalId for o in getattr(new, "HasOpenings", []) or []] new_openings = sorted([o.RelatedOpeningElement.GlobalId for o in getattr(new, "HasOpenings", []) or []])
if old_openings != new_openings: if old_openings != new_openings:
return True return True
old_projections = [o.RelatedFeatureElement.GlobalId for o in getattr(old, "HasProjections", []) or []] old_projections = sorted([o.RelatedFeatureElement.GlobalId for o in getattr(old, "HasProjections", []) or []])
new_projections = [o.RelatedFeatureElement.GlobalId for o in getattr(new, "HasProjections", []) or []] new_projections = sorted([o.RelatedFeatureElement.GlobalId for o in getattr(new, "HasProjections", []) or []])
if old_projections != new_projections: if old_projections != new_projections:
return True return True
old_rep_id = self.get_representation_id(old) # Option 3: check completely using Python with get_info_2 (extremely slow, not worth it)
new_rep_id = self.get_representation_id(new) # old_rep_id = self.get_representation_id(old)
rep_result = self.representation_ids.get(new_rep_id, None) # new_rep_id = self.get_representation_id(new)
if rep_result is not None: # rep_result = self.representation_ids.get(new_rep_id, None)
return rep_result # if rep_result is not None:
if type(old_rep_id) != type(new_rep_id): # return rep_result
self.representation_ids[new_rep_id] = True # if type(old_rep_id) != type(new_rep_id):
return True # self.representation_ids[new_rep_id] = True
if new_rep_id is None: # return True
return # if new_rep_id is None:
result = self.diff_representation(old_rep_id, new_rep_id) or False # return
self.representation_ids[new_rep_id] = result # result = self.diff_representation(old_rep_id, new_rep_id) or False
return result # self.representation_ids[new_rep_id] = result
# return result
def diff_representation(self, old_rep_id, new_rep_id): def diff_representation(self, old_rep_id, new_rep_id):
old_rep = self.old.by_id(old_rep_id) old_rep = self.old.by_id(old_rep_id)
@@ -290,10 +413,6 @@ if __name__ == "__main__":
ifc_diff = IfcDiff(old, new, args.relationships.split()) ifc_diff = IfcDiff(old, new, args.relationships.split())
ifc_diff.diff() ifc_diff.diff()
print(" - {} item(s) were deleted".format(len(ifc_diff.deleted_elements)))
print(" - {} item(s) were added".format(len(ifc_diff.added_elements)))
print(" - {} item(s) were changed".format(len(ifc_diff.change_register.keys())))
print("# Diff finished in {:.2f} seconds".format(time.time() - start)) print("# Diff finished in {:.2f} seconds".format(time.time() - start))
ifc_diff.export(args.output) ifc_diff.export(args.output)