mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-11 14:22:33 +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 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)
|
||||||
|
|||||||
Reference in New Issue
Block a user