More IfcDiff optimisations (461s->247s), bump DeepDiff version, less false positives with more accurate number comparisons

This commit is contained in:
Dion Moult
2022-08-04 17:49:18 +10:00
parent 1629205c3f
commit a4d0378405
2 changed files with 133 additions and 113 deletions
+3 -3
View File
@@ -210,9 +210,9 @@ endif
# Required by IFCDiff # Required by IFCDiff
mkdir dist/working mkdir dist/working
cd dist/working && wget https://github.com/Moult/deepdiff/archive/master.zip cd dist/working && wget https://files.pythonhosted.org/packages/0f/ca/caead2949fbb824c7142e3774fa841aa853bb4d4331b440da8c8514dfc6f/deepdiff-5.8.1.tar.gz
cd dist/working && unzip master.zip cd dist/working && tar -xzvf deepdiff*
cp -r dist/working/deepdiff-master/deepdiff dist/blenderbim/libs/site/packages/ cp -r dist/working/deepdiff-5.8.1/deepdiff dist/blenderbim/libs/site/packages/
rm -rf dist/working rm -rf dist/working
# Required by deepdiff # Required by deepdiff
+130 -110
View File
@@ -20,27 +20,32 @@
# This can be packaged with `pyinstaller --onefile --clean --icon=icon.ico ifcdiff.py` # This can be packaged with `pyinstaller --onefile --clean --icon=icon.ico ifcdiff.py`
import ifcopenshell
from deepdiff import DeepDiff
import time import time
import json import json
import logging
import argparse import argparse
import decimal import numpy as np
import ifcopenshell
import ifcopenshell.util.element
import ifcopenshell.util.placement
import ifcopenshell.util.classification
from deepdiff import DeepDiff
class IfcDiff: class IfcDiff:
def __init__(self, old_file, new_file, output_file, inverse_classes=None, is_shallow=False): def __init__(self, old_file, new_file, output_file, relationships=None, is_shallow=True):
self.old_file = old_file self.old_file = old_file
self.new_file = new_file self.new_file = new_file
self.output_file = output_file self.output_file = output_file
self.change_register = {} self.change_register = {}
self.representation_ids = set() self.representation_ids = {}
self.inverse_classes = inverse_classes self.relationships = relationships
self.precision = 2 self.precision = 1e-4
self.is_shallow = is_shallow self.is_shallow = is_shallow
def diff(self): def diff(self):
print("# IFC Diff") print("# IFC Diff")
logging.disable(logging.CRITICAL)
self.load() self.load()
self.precision = self.get_precision() self.precision = self.get_precision()
@@ -64,20 +69,19 @@ class IfcDiff:
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_element = self.old.by_id(global_id) old = self.old.by_id(global_id)
new_element = self.new.by_id(global_id) new = self.new.by_id(global_id)
if self.diff_element(old_element, new_element) and self.is_shallow: if self.diff_element(old, new) and self.is_shallow:
continue continue
if self.diff_element_inverse_relationships(old_element, new_element) and self.is_shallow: if self.diff_element_relationships(old, new) and self.is_shallow:
continue continue
representation_id = self.get_representation_id(new_element) diff = self.diff_element_geometry(old, new)
if representation_id in self.representation_ids: if diff:
continue self.change_register.setdefault(new.GlobalId, {}).update({"geometry_changed": True})
self.representation_ids.add(representation_id)
self.diff_element_geometry(old_element, new_element)
print(" - {} item(s) were changed either geometrically or with data".format(len(self.change_register.keys()))) print(" - {} item(s) were changed either geometrically or with data".format(len(self.change_register.keys())))
print("# Diff finished in {:.2f} seconds".format(time.time() - start)) print("# Diff finished in {:.2f} seconds".format(time.time() - start))
logging.disable(logging.NOTSET)
def export(self): def export(self):
with open(self.output_file, "w", encoding="utf-8") as diff_file: with open(self.output_file, "w", encoding="utf-8") as diff_file:
@@ -99,116 +103,123 @@ class IfcDiff:
self.new = ifcopenshell.open(self.new_file) self.new = ifcopenshell.open(self.new_file)
def get_precision(self): def get_precision(self):
try: contexts = [c for c in self.new.by_type("IfcGeometricRepresentationContext") if c.ContextType == "Model"]
precision = [c for c in self.new.by_type("IfcGeometricRepresentationContext") if c.ContextType == "Model"][ if contexts:
0 return contexts[0].Precision or 1e-4
].Precision return 1e-4
exponent = decimal.Decimal(str(precision)).as_tuple().exponent
if exponent < 0:
return abs(exponent)
return 0
except:
return 2
def diff_element(self, old_element, new_element): def diff_element(self, old, new):
diff = DeepDiff( diff = DeepDiff(
old_element, [a for a in old if not isinstance(a, (ifcopenshell.entity_instance, tuple))],
new_element, [a for a in new if not isinstance(a, (ifcopenshell.entity_instance, tuple))],
significant_digits=self.precision, math_epsilon=self.precision,
ignore_string_type_changes=True, ignore_string_type_changes=True,
ignore_numeric_type_changes=True, ignore_numeric_type_changes=True,
exclude_regex_paths={
r"root.*id$",
r".*Representation.*",
r".*OwnerHistory.*",
r".*ObjectPlacement.*",
},
) )
if diff and new_element.GlobalId: if diff and new.GlobalId:
self.change_register.setdefault(new_element.GlobalId, {}).update(diff) self.change_register.setdefault(new.GlobalId, {}).update({"attributes_changed": True})
return True return True
def diff_element_inverse_relationships(self, old_element, new_element): def diff_element_relationships(self, old, new):
if not self.inverse_classes: if not self.relationships:
return return
old_relationships_all = self.old.get_inverse(old_element) for relationship in self.relationships:
new_relationships_all = self.new.get_inverse(new_element) if relationship == "type":
if self.inverse_classes[0] == "all": if ifcopenshell.util.element.get_type(old) != ifcopenshell.util.element.get_type(new):
old_relationships = old_relationships_all self.change_register.setdefault(new.GlobalId, {}).update({"type_changed": True})
new_relationships = new_relationships_all return True
else: elif relationship == "property":
old_relationships = [x for x in old_relationships_all if x.is_a() in self.inverse_classes] old_psets = ifcopenshell.util.element.get_psets(old)
new_relationships = [x for x in new_relationships_all if x.is_a() in self.inverse_classes] new_psets = ifcopenshell.util.element.get_psets(new)
try:
diff = DeepDiff(
old_psets,
new_psets,
math_epsilon=self.precision,
ignore_string_type_changes=True,
ignore_numeric_type_changes=True,
exclude_regex_paths=[r".*id$"],
)
except:
diff = True
if diff and new.GlobalId:
self.change_register.setdefault(new.GlobalId, {}).update({"properties_changed": diff})
return True
elif relationship == "container":
if ifcopenshell.util.element.get_container(old) != ifcopenshell.util.element.get_container(new):
self.change_register.setdefault(new.GlobalId, {}).update({"container_changed": True})
return True
elif relationship == "aggregate":
if ifcopenshell.util.element.get_aggregate(old) != ifcopenshell.util.element.get_aggregate(new):
self.change_register.setdefault(new.GlobalId, {}).update({"aggregate_changed": True})
return True
elif relationship == "classification":
old_id = "ItemReference" if self.old.schema == "IFC2X3" else "Identification"
new_id = "ItemReference" if self.new.schema == "IFC2X3" else "Identification"
old_refs = [getattr(r, old_id) for r in ifcopenshell.util.classification.get_references(old)]
new_refs = [getattr(r, new_id) for r in ifcopenshell.util.classification.get_references(new)]
if old_refs != new_refs:
self.change_register.setdefault(new.GlobalId, {}).update({"classification_changed": True})
return True
diff = DeepDiff( def diff_element_geometry(self, old, new):
old_relationships, old_placement = ifcopenshell.util.placement.get_local_placement(old.ObjectPlacement)
new_relationships, new_placement = ifcopenshell.util.placement.get_local_placement(new.ObjectPlacement)
significant_digits=self.precision, if not np.allclose(old_placement[:,3], new_placement[:,3], atol=self.precision):
ignore_string_type_changes=True,
ignore_numeric_type_changes=True,
exclude_regex_paths=[
r"root.*id$",
r".*GlobalId.*",
r".*OwnerHistory.*",
r".*RelatedObjects.*",
r".*RelatingObject.*",
r".*RelatingDefinitions.*",
r".*RelatedObjectsType.*", # Deprecated in IFC4 anyway
],
)
if diff and new_element.GlobalId:
self.change_register.setdefault(new_element.GlobalId, {}).update(diff)
return True 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 []]
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 []]
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
def diff_element_geometry(self, old_element, new_element): def diff_representation(self, old_rep_id, new_rep_id):
old_rep = self.old.by_id(old_rep_id)
new_rep = self.new.by_id(new_rep_id)
if len(old_rep.Items) != len(new_rep.Items):
return True
for i, old_item in enumerate(old_rep.Items):
result = self.diff_representation_item(old_item, new_rep.Items[i])
if result is True:
return True
def diff_representation_item(self, old_item, new_item):
if old_item.is_a() != new_item.is_a():
return True
try: try:
DeepDiff( diff = DeepDiff(
old_element.ObjectPlacement, old_item.get_info_2(recursive=True),
new_element.ObjectPlacement, new_item.get_info_2(recursive=True),
terminate_on_first=True, custom_operators=[DiffTerminator()] if self.is_shallow else [],
significant_digits=self.precision, math_epsilon=self.precision,
ignore_string_type_changes=True, exclude_regex_paths=[r".*id']$"]
ignore_numeric_type_changes=True,
exclude_regex_paths=r"root.*id$",
)
DeepDiff(
old_element.HasOpenings,
new_element.HasOpenings,
terminate_on_first=True,
significant_digits=self.precision,
ignore_string_type_changes=True,
ignore_numeric_type_changes=True,
exclude_regex_paths=r"root.*id$",
)
DeepDiff(
old_element.HasProjections,
new_element.HasProjections,
terminate_on_first=True,
significant_digits=self.precision,
ignore_string_type_changes=True,
ignore_numeric_type_changes=True,
exclude_regex_paths=r"root.*id$",
)
DeepDiff(
old_element.Representation.get_info_2(recursive=True),
new_element.Representation.get_info_2(recursive=True),
terminate_on_first=True,
skip_after_n=500, # Arbitrary value to "skim" check
significant_digits=self.precision,
ignore_string_type_changes=True,
ignore_numeric_type_changes=True,
exclude_regex_paths=[
r"root.*id']$",
r".*ContextOfItems.*",
],
) )
except: except:
if new_element.GlobalId: return True
return self.change_register.setdefault(new_element.GlobalId, {}).update({"has_geometry_change": True}) if diff:
return True
def get_representation_id(self, element): def get_representation_id(self, element):
if not element.Representation: if not element.Representation:
return None return
for representation in element.Representation.Representations: for representation in element.Representation.Representations:
if not representation.is_a("IfcShapeRepresentation"): if not representation.is_a("IfcShapeRepresentation"):
continue continue
@@ -221,6 +232,15 @@ class IfcDiff:
return representation.Items[0].MappingSource.MappedRepresentation.id() return representation.Items[0].MappingSource.MappedRepresentation.id()
class DiffTerminator:
def match(self, level) -> bool:
return True
def give_up_diffing(self, level, diff_instance) -> bool:
if any(diff_instance.tree.values()):
raise Exception("Terminated")
class DiffEncoder(json.JSONEncoder): class DiffEncoder(json.JSONEncoder):
def default(self, obj): def default(self, obj):
try: try:
@@ -240,7 +260,7 @@ if __name__ == "__main__":
"-r", "-r",
"--relationships", "--relationships",
type=str, type=str,
help='A list of IFC classes to check in inverse relationships, like "IfcRelDefinesByProperties", or "all".', help='A list of space-separated relationships, chosen from "type", "property", "container", "aggregate", "classification"',
default="", default="",
) )
args = parser.parse_args() args = parser.parse_args()