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 argparse
import numpy as np
import multiprocessing
import ifcopenshell
import ifcopenshell.geom
import ifcopenshell.util.element
import ifcopenshell.util.placement
import ifcopenshell.util.classification
@@ -42,8 +44,8 @@ class IfcDiff:
:type old: ifcopenshell.file.file
:param new: IFC file object for the old model
:type new: ifcopenshell.file.file
:param relationships: List of relationships to check. An empty list means
that only geometry and attributes are compared.
:param relationships: List of relationships to check. None means that only
geometry is compared.
:type relationships: list[string]
: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
@@ -68,7 +70,7 @@ class IfcDiff:
self.new = new
self.change_register = {}
self.representation_ids = {}
self.relationships = relationships
self.relationships = relationships or ["geometry"]
self.precision = 1e-4
self.is_shallow = is_shallow
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))
new_elements = set(e.GlobalId for e in selector.parse(self.new, self.filter_elements))
else:
old_elements = set(e.GlobalId for e in self.old.by_type("IfcProduct"))
new_elements = set(e.GlobalId for e in self.new.by_type("IfcProduct"))
old_elements = self.old.by_type("IfcElement")
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.added_elements = new_elements - old_elements
same_elements = new_elements - self.added_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
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:
total_diffed += 1
if total_diffed % 250 == 0:
print("{}/{} diffed ...".format(total_diffed, total_same_elements), end="\r", flush=True)
old = self.old.by_id(global_id)
new = self.new.by_id(global_id)
if self.diff_element(old, new) and self.is_shallow:
continue
if self.diff_element_relationships(old, new) and self.is_shallow:
continue
diff = self.diff_element_geometry(old, new)
if diff:
self.change_register.setdefault(new.GlobalId, {}).update({"geometry_changed": True})
if should_check_attributes:
if self.diff_element(old, new) and self.is_shallow:
continue
if should_check_other:
if self.diff_element_relationships(old, new) and self.is_shallow:
continue
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)
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):
with open(path, "w", encoding="utf-8") as diff_file:
json.dump(
@@ -181,34 +303,35 @@ class IfcDiff:
self.change_register.setdefault(new.GlobalId, {}).update({"classification_changed": 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)
new_placement = ifcopenshell.util.placement.get_local_placement(new.ObjectPlacement)
if not np.allclose(old_placement[:, 3], new_placement[:, 3], atol=self.precision):
return True
if not np.allclose(old_placement[0:3, 0:3], new_placement[0:3, 0:3], atol=1e-2):
return True
old_openings = [o.RelatedOpeningElement.GlobalId for o in getattr(old, "HasOpenings", []) or []]
new_openings = [o.RelatedOpeningElement.GlobalId for o in getattr(new, "HasOpenings", []) or []]
old_openings = sorted([o.RelatedOpeningElement.GlobalId for o in getattr(old, "HasOpenings", []) or []])
new_openings = sorted([o.RelatedOpeningElement.GlobalId for o in getattr(new, "HasOpenings", []) or []])
if old_openings != new_openings:
return True
old_projections = [o.RelatedFeatureElement.GlobalId for o in getattr(old, "HasProjections", []) or []]
new_projections = [o.RelatedFeatureElement.GlobalId for o in getattr(new, "HasProjections", []) or []]
old_projections = sorted([o.RelatedFeatureElement.GlobalId for o in getattr(old, "HasProjections", []) or []])
new_projections = sorted([o.RelatedFeatureElement.GlobalId for o in getattr(new, "HasProjections", []) or []])
if old_projections != new_projections:
return True
old_rep_id = self.get_representation_id(old)
new_rep_id = self.get_representation_id(new)
rep_result = self.representation_ids.get(new_rep_id, None)
if rep_result is not None:
return rep_result
if type(old_rep_id) != type(new_rep_id):
self.representation_ids[new_rep_id] = True
return True
if new_rep_id is None:
return
result = self.diff_representation(old_rep_id, new_rep_id) or False
self.representation_ids[new_rep_id] = result
return result
# Option 3: check completely using Python with get_info_2 (extremely slow, not worth it)
# old_rep_id = self.get_representation_id(old)
# new_rep_id = self.get_representation_id(new)
# rep_result = self.representation_ids.get(new_rep_id, None)
# if rep_result is not None:
# return rep_result
# if type(old_rep_id) != type(new_rep_id):
# self.representation_ids[new_rep_id] = True
# return True
# if new_rep_id is None:
# return
# result = self.diff_representation(old_rep_id, new_rep_id) or False
# self.representation_ids[new_rep_id] = result
# return result
def diff_representation(self, old_rep_id, new_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.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))
ifc_diff.export(args.output)