Skip to content

Commit a4d0378

Browse files
committed
More IfcDiff optimisations (461s->247s), bump DeepDiff version, less false positives with more accurate number comparisons
1 parent 1629205 commit a4d0378

File tree

2 files changed

+133
-113
lines changed

2 files changed

+133
-113
lines changed

src/blenderbim/Makefile

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -210,9 +210,9 @@ endif
210210

211211
# Required by IFCDiff
212212
mkdir dist/working
213-
cd dist/working && wget https://github.com/Moult/deepdiff/archive/master.zip
214-
cd dist/working && unzip master.zip
215-
cp -r dist/working/deepdiff-master/deepdiff dist/blenderbim/libs/site/packages/
213+
cd dist/working && wget https://files.pythonhosted.org/packages/0f/ca/caead2949fbb824c7142e3774fa841aa853bb4d4331b440da8c8514dfc6f/deepdiff-5.8.1.tar.gz
214+
cd dist/working && tar -xzvf deepdiff*
215+
cp -r dist/working/deepdiff-5.8.1/deepdiff dist/blenderbim/libs/site/packages/
216216
rm -rf dist/working
217217

218218
# Required by deepdiff

src/ifcdiff/ifcdiff.py

Lines changed: 130 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -20,27 +20,32 @@
2020

2121
# This can be packaged with `pyinstaller --onefile --clean --icon=icon.ico ifcdiff.py`
2222

23-
import ifcopenshell
24-
from deepdiff import DeepDiff
2523
import time
2624
import json
25+
import logging
2726
import argparse
28-
import decimal
27+
import numpy as np
28+
import ifcopenshell
29+
import ifcopenshell.util.element
30+
import ifcopenshell.util.placement
31+
import ifcopenshell.util.classification
32+
from deepdiff import DeepDiff
2933

3034

3135
class IfcDiff:
32-
def __init__(self, old_file, new_file, output_file, inverse_classes=None, is_shallow=False):
36+
def __init__(self, old_file, new_file, output_file, relationships=None, is_shallow=True):
3337
self.old_file = old_file
3438
self.new_file = new_file
3539
self.output_file = output_file
3640
self.change_register = {}
37-
self.representation_ids = set()
38-
self.inverse_classes = inverse_classes
39-
self.precision = 2
41+
self.representation_ids = {}
42+
self.relationships = relationships
43+
self.precision = 1e-4
4044
self.is_shallow = is_shallow
4145

4246
def diff(self):
4347
print("# IFC Diff")
48+
logging.disable(logging.CRITICAL)
4449
self.load()
4550

4651
self.precision = self.get_precision()
@@ -64,20 +69,19 @@ def diff(self):
6469
total_diffed += 1
6570
if total_diffed % 250 == 0:
6671
print("{}/{} diffed ...".format(total_diffed, total_same_elements), end="\r", flush=True)
67-
old_element = self.old.by_id(global_id)
68-
new_element = self.new.by_id(global_id)
69-
if self.diff_element(old_element, new_element) and self.is_shallow:
72+
old = self.old.by_id(global_id)
73+
new = self.new.by_id(global_id)
74+
if self.diff_element(old, new) and self.is_shallow:
7075
continue
71-
if self.diff_element_inverse_relationships(old_element, new_element) and self.is_shallow:
76+
if self.diff_element_relationships(old, new) and self.is_shallow:
7277
continue
73-
representation_id = self.get_representation_id(new_element)
74-
if representation_id in self.representation_ids:
75-
continue
76-
self.representation_ids.add(representation_id)
77-
self.diff_element_geometry(old_element, new_element)
78+
diff = self.diff_element_geometry(old, new)
79+
if diff:
80+
self.change_register.setdefault(new.GlobalId, {}).update({"geometry_changed": True})
7881

7982
print(" - {} item(s) were changed either geometrically or with data".format(len(self.change_register.keys())))
8083
print("# Diff finished in {:.2f} seconds".format(time.time() - start))
84+
logging.disable(logging.NOTSET)
8185

8286
def export(self):
8387
with open(self.output_file, "w", encoding="utf-8") as diff_file:
@@ -99,116 +103,123 @@ def load(self):
99103
self.new = ifcopenshell.open(self.new_file)
100104

101105
def get_precision(self):
102-
try:
103-
precision = [c for c in self.new.by_type("IfcGeometricRepresentationContext") if c.ContextType == "Model"][
104-
0
105-
].Precision
106-
exponent = decimal.Decimal(str(precision)).as_tuple().exponent
107-
if exponent < 0:
108-
return abs(exponent)
109-
return 0
110-
except:
111-
return 2
106+
contexts = [c for c in self.new.by_type("IfcGeometricRepresentationContext") if c.ContextType == "Model"]
107+
if contexts:
108+
return contexts[0].Precision or 1e-4
109+
return 1e-4
112110

113-
def diff_element(self, old_element, new_element):
111+
def diff_element(self, old, new):
114112
diff = DeepDiff(
115-
old_element,
116-
new_element,
117-
significant_digits=self.precision,
113+
[a for a in old if not isinstance(a, (ifcopenshell.entity_instance, tuple))],
114+
[a for a in new if not isinstance(a, (ifcopenshell.entity_instance, tuple))],
115+
math_epsilon=self.precision,
118116
ignore_string_type_changes=True,
119117
ignore_numeric_type_changes=True,
120-
exclude_regex_paths={
121-
r"root.*id$",
122-
r".*Representation.*",
123-
r".*OwnerHistory.*",
124-
r".*ObjectPlacement.*",
125-
},
126118
)
127-
if diff and new_element.GlobalId:
128-
self.change_register.setdefault(new_element.GlobalId, {}).update(diff)
119+
if diff and new.GlobalId:
120+
self.change_register.setdefault(new.GlobalId, {}).update({"attributes_changed": True})
129121
return True
130122

131-
def diff_element_inverse_relationships(self, old_element, new_element):
132-
if not self.inverse_classes:
123+
def diff_element_relationships(self, old, new):
124+
if not self.relationships:
133125
return
134-
old_relationships_all = self.old.get_inverse(old_element)
135-
new_relationships_all = self.new.get_inverse(new_element)
136-
if self.inverse_classes[0] == "all":
137-
old_relationships = old_relationships_all
138-
new_relationships = new_relationships_all
139-
else:
140-
old_relationships = [x for x in old_relationships_all if x.is_a() in self.inverse_classes]
141-
new_relationships = [x for x in new_relationships_all if x.is_a() in self.inverse_classes]
126+
for relationship in self.relationships:
127+
if relationship == "type":
128+
if ifcopenshell.util.element.get_type(old) != ifcopenshell.util.element.get_type(new):
129+
self.change_register.setdefault(new.GlobalId, {}).update({"type_changed": True})
130+
return True
131+
elif relationship == "property":
132+
old_psets = ifcopenshell.util.element.get_psets(old)
133+
new_psets = ifcopenshell.util.element.get_psets(new)
134+
try:
135+
diff = DeepDiff(
136+
old_psets,
137+
new_psets,
138+
math_epsilon=self.precision,
139+
ignore_string_type_changes=True,
140+
ignore_numeric_type_changes=True,
141+
exclude_regex_paths=[r".*id$"],
142+
)
143+
except:
144+
diff = True
145+
if diff and new.GlobalId:
146+
self.change_register.setdefault(new.GlobalId, {}).update({"properties_changed": diff})
147+
return True
148+
elif relationship == "container":
149+
if ifcopenshell.util.element.get_container(old) != ifcopenshell.util.element.get_container(new):
150+
self.change_register.setdefault(new.GlobalId, {}).update({"container_changed": True})
151+
return True
152+
elif relationship == "aggregate":
153+
if ifcopenshell.util.element.get_aggregate(old) != ifcopenshell.util.element.get_aggregate(new):
154+
self.change_register.setdefault(new.GlobalId, {}).update({"aggregate_changed": True})
155+
return True
156+
elif relationship == "classification":
157+
old_id = "ItemReference" if self.old.schema == "IFC2X3" else "Identification"
158+
new_id = "ItemReference" if self.new.schema == "IFC2X3" else "Identification"
159+
old_refs = [getattr(r, old_id) for r in ifcopenshell.util.classification.get_references(old)]
160+
new_refs = [getattr(r, new_id) for r in ifcopenshell.util.classification.get_references(new)]
161+
if old_refs != new_refs:
162+
self.change_register.setdefault(new.GlobalId, {}).update({"classification_changed": True})
163+
return True
142164

143-
diff = DeepDiff(
144-
old_relationships,
145-
new_relationships,
146-
significant_digits=self.precision,
147-
ignore_string_type_changes=True,
148-
ignore_numeric_type_changes=True,
149-
exclude_regex_paths=[
150-
r"root.*id$",
151-
r".*GlobalId.*",
152-
r".*OwnerHistory.*",
153-
r".*RelatedObjects.*",
154-
r".*RelatingObject.*",
155-
r".*RelatingDefinitions.*",
156-
r".*RelatedObjectsType.*", # Deprecated in IFC4 anyway
157-
],
158-
)
159-
if diff and new_element.GlobalId:
160-
self.change_register.setdefault(new_element.GlobalId, {}).update(diff)
165+
def diff_element_geometry(self, old, new):
166+
old_placement = ifcopenshell.util.placement.get_local_placement(old.ObjectPlacement)
167+
new_placement = ifcopenshell.util.placement.get_local_placement(new.ObjectPlacement)
168+
if not np.allclose(old_placement[:,3], new_placement[:,3], atol=self.precision):
169+
return True
170+
if not np.allclose(old_placement[0:3,0:3], new_placement[0:3,0:3], atol=1e-2):
171+
return True
172+
old_openings = [o.RelatedOpeningElement.GlobalId for o in getattr(old, "HasOpenings", []) or []]
173+
new_openings = [o.RelatedOpeningElement.GlobalId for o in getattr(new, "HasOpenings", []) or []]
174+
if old_openings != new_openings:
175+
return True
176+
old_projections = [o.RelatedFeatureElement.GlobalId for o in getattr(old, "HasProjections", []) or []]
177+
new_projections = [o.RelatedFeatureElement.GlobalId for o in getattr(new, "HasProjections", []) or []]
178+
if old_projections != new_projections:
161179
return True
180+
old_rep_id = self.get_representation_id(old)
181+
new_rep_id = self.get_representation_id(new)
182+
rep_result = self.representation_ids.get(new_rep_id, None)
183+
if rep_result is not None:
184+
return rep_result
185+
if type(old_rep_id) != type(new_rep_id):
186+
self.representation_ids[new_rep_id] = True
187+
return True
188+
if new_rep_id is None:
189+
return
190+
result = self.diff_representation(old_rep_id, new_rep_id) or False
191+
self.representation_ids[new_rep_id] = result
192+
return result
193+
194+
def diff_representation(self, old_rep_id, new_rep_id):
195+
old_rep = self.old.by_id(old_rep_id)
196+
new_rep = self.new.by_id(new_rep_id)
197+
if len(old_rep.Items) != len(new_rep.Items):
198+
return True
199+
for i, old_item in enumerate(old_rep.Items):
200+
result = self.diff_representation_item(old_item, new_rep.Items[i])
201+
if result is True:
202+
return True
162203

163-
def diff_element_geometry(self, old_element, new_element):
204+
def diff_representation_item(self, old_item, new_item):
205+
if old_item.is_a() != new_item.is_a():
206+
return True
164207
try:
165-
DeepDiff(
166-
old_element.ObjectPlacement,
167-
new_element.ObjectPlacement,
168-
terminate_on_first=True,
169-
significant_digits=self.precision,
170-
ignore_string_type_changes=True,
171-
ignore_numeric_type_changes=True,
172-
exclude_regex_paths=r"root.*id$",
173-
)
174-
DeepDiff(
175-
old_element.HasOpenings,
176-
new_element.HasOpenings,
177-
terminate_on_first=True,
178-
significant_digits=self.precision,
179-
ignore_string_type_changes=True,
180-
ignore_numeric_type_changes=True,
181-
exclude_regex_paths=r"root.*id$",
182-
)
183-
DeepDiff(
184-
old_element.HasProjections,
185-
new_element.HasProjections,
186-
terminate_on_first=True,
187-
significant_digits=self.precision,
188-
ignore_string_type_changes=True,
189-
ignore_numeric_type_changes=True,
190-
exclude_regex_paths=r"root.*id$",
191-
)
192-
DeepDiff(
193-
old_element.Representation.get_info_2(recursive=True),
194-
new_element.Representation.get_info_2(recursive=True),
195-
terminate_on_first=True,
196-
skip_after_n=500, # Arbitrary value to "skim" check
197-
significant_digits=self.precision,
198-
ignore_string_type_changes=True,
199-
ignore_numeric_type_changes=True,
200-
exclude_regex_paths=[
201-
r"root.*id']$",
202-
r".*ContextOfItems.*",
203-
],
208+
diff = DeepDiff(
209+
old_item.get_info_2(recursive=True),
210+
new_item.get_info_2(recursive=True),
211+
custom_operators=[DiffTerminator()] if self.is_shallow else [],
212+
math_epsilon=self.precision,
213+
exclude_regex_paths=[r".*id']$"]
204214
)
205215
except:
206-
if new_element.GlobalId:
207-
return self.change_register.setdefault(new_element.GlobalId, {}).update({"has_geometry_change": True})
216+
return True
217+
if diff:
218+
return True
208219

209220
def get_representation_id(self, element):
210221
if not element.Representation:
211-
return None
222+
return
212223
for representation in element.Representation.Representations:
213224
if not representation.is_a("IfcShapeRepresentation"):
214225
continue
@@ -221,6 +232,15 @@ def get_representation_id(self, element):
221232
return representation.Items[0].MappingSource.MappedRepresentation.id()
222233

223234

235+
class DiffTerminator:
236+
def match(self, level) -> bool:
237+
return True
238+
239+
def give_up_diffing(self, level, diff_instance) -> bool:
240+
if any(diff_instance.tree.values()):
241+
raise Exception("Terminated")
242+
243+
224244
class DiffEncoder(json.JSONEncoder):
225245
def default(self, obj):
226246
try:
@@ -240,7 +260,7 @@ def default(self, obj):
240260
"-r",
241261
"--relationships",
242262
type=str,
243-
help='A list of IFC classes to check in inverse relationships, like "IfcRelDefinesByProperties", or "all".',
263+
help='A list of space-separated relationships, chosen from "type", "property", "container", "aggregate", "classification"',
244264
default="",
245265
)
246266
args = parser.parse_args()

0 commit comments

Comments
 (0)