mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-10 01:41:57 +00:00
IfcDiff now uses the iterator to compare geometry, which seems significantly faster.
This commit is contained in:
+153
-34
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user