490 lines
14 KiB
Python
490 lines
14 KiB
Python
import ifcopenshell
|
|
import ifcopenshell.geom
|
|
|
|
import itertools
|
|
import os
|
|
|
|
import xlsxwriter
|
|
import csv
|
|
|
|
|
|
def is_applicability(concept):
|
|
"""
|
|
Check whether the Concept created has a filtering purpose.
|
|
Actually, MvdXML has a specific Applicability node.
|
|
|
|
:param concept: mvdXML Concept object
|
|
"""
|
|
return concept.name.startswith("AP")
|
|
|
|
|
|
def merge_dictionaries(dicts):
|
|
d = {}
|
|
for e in dicts:
|
|
d.update(e)
|
|
return d
|
|
|
|
|
|
def extract_data(mvd_node, ifc_data):
|
|
"""
|
|
Recursively traverses mvdXML Concept tree structure.
|
|
This tree is made of different mvdXML Rule nodes: AttributesRule
|
|
and EntityRule.
|
|
|
|
:param mvd_node: an mvdXML Concept
|
|
:param ifc_data: an IFC instance or an IFC value
|
|
|
|
|
|
"""
|
|
to_combine = []
|
|
return_value = []
|
|
|
|
if len(mvd_node.nodes) == 0:
|
|
if mvd_node.tag == "AttributeRule":
|
|
try:
|
|
values_from_attribute = getattr(ifc_data, mvd_node.attribute)
|
|
return [{mvd_node: values_from_attribute}]
|
|
except:
|
|
return [{mvd_node: "Invalid Attribute"}]
|
|
|
|
else:
|
|
return [{mvd_node: ifc_data}]
|
|
|
|
if mvd_node.tag == 'AttributeRule':
|
|
data_from_attribute = []
|
|
try:
|
|
values_from_attribute = getattr(ifc_data, mvd_node.attribute)
|
|
if values_from_attribute is None:
|
|
return [{mvd_node:"Nonexistent value"}]
|
|
|
|
except:
|
|
return [{mvd_node:"Invalid attribute rule"}]
|
|
|
|
|
|
if isinstance(values_from_attribute, (list, tuple)):
|
|
if len(values_from_attribute) == 0:
|
|
return [{mvd_node: 'empty data structure'}]
|
|
data_from_attribute.extend(values_from_attribute)
|
|
|
|
else:
|
|
data_from_attribute.append(values_from_attribute)
|
|
|
|
for child in mvd_node.nodes:
|
|
for data in data_from_attribute:
|
|
child_values = extract_data(child, data)
|
|
if isinstance(child_values, (list, tuple)):
|
|
return_value.extend(child_values)
|
|
else:
|
|
return_value.append(child_values)
|
|
return return_value
|
|
|
|
elif mvd_node.tag == 'EntityRule':
|
|
# Avoid things like Quantities on Psets
|
|
if len(mvd_node.nodes):
|
|
if isinstance(ifc_data, ifcopenshell.entity_instance) and not ifc_data.is_a(mvd_node.attribute):
|
|
return []
|
|
|
|
for child in mvd_node.nodes:
|
|
if child.tag == "Constraint":
|
|
on_node = child.attribute[0].c
|
|
on_node = on_node.replace("'", "")
|
|
if isinstance(ifc_data, ifcopenshell.entity_instance):
|
|
ifc_type = type(ifc_data[0])
|
|
typed_node = (ifc_type)(on_node)
|
|
|
|
if ifc_data[0] == typed_node:
|
|
return [{mvd_node: ifc_data}]
|
|
|
|
elif ifc_data == on_node:
|
|
return [{mvd_node: ifc_data}]
|
|
else:
|
|
to_combine.append(extract_data(child, ifc_data))
|
|
|
|
if len(to_combine):
|
|
return_value = list(map(merge_dictionaries, itertools.product(*to_combine)))
|
|
|
|
return return_value
|
|
|
|
|
|
def open_mvd(filename):
|
|
"""
|
|
Open an mvdXML file.
|
|
|
|
:param filename: Path of the mvdXML file.
|
|
:return: mvdXML Concept instance.
|
|
"""
|
|
my_concept_object = list(ifcopenshell.mvd.concept_root.parse(filename))[0]
|
|
return my_concept_object
|
|
|
|
|
|
def format_data_from_nodes(recurse_output):
|
|
"""
|
|
Enable to format data collected such that the value to be exported is extracted.
|
|
|
|
:param recurse_output: Data extracted from the recursive function
|
|
|
|
"""
|
|
if len(recurse_output) > 1:
|
|
output = []
|
|
for resulting_dict in recurse_output:
|
|
intermediate_storing = []
|
|
for value in resulting_dict.values():
|
|
intermediate_storing.append(value)
|
|
output.extend(intermediate_storing)
|
|
return output
|
|
|
|
elif len(recurse_output) == 1:
|
|
return_list = []
|
|
intermediate_list = list(recurse_output[0].values())
|
|
if len(intermediate_list) > 1:
|
|
returned_value = intermediate_list
|
|
for element in intermediate_list:
|
|
# In case of a property that comes with all its path
|
|
# (like ['PSet_WallCommon, 'IsExternal', IfcBoolean(.F.)
|
|
# return only the list element which is not of string type
|
|
# todo: check above condition with ifcopenshell type
|
|
if not isinstance(element, str):
|
|
returned_value = element
|
|
if returned_value != intermediate_list:
|
|
return returned_value
|
|
else:
|
|
return intermediate_list
|
|
else:
|
|
return intermediate_list[0]
|
|
|
|
else:
|
|
return []
|
|
|
|
|
|
def get_data_from_mvd(entities, tree, filtering=False):
|
|
"""
|
|
Apply the recursive function on the entities to return
|
|
the values extracted.
|
|
|
|
:param entities: IFC instances to be processed.
|
|
:param tree: mvdXML Concept instance tree root.
|
|
:param filtering: Indicates whether the mvdXML tree is an applicability.
|
|
|
|
"""
|
|
filtered_entities = []
|
|
extracted_entities_data = {}
|
|
|
|
for entity in entities:
|
|
entity_id = entity.GlobalId
|
|
combinations = extract_data(tree, entity)
|
|
desired_results = []
|
|
|
|
for dictionary in combinations:
|
|
desired_results.append(dictionary)
|
|
|
|
output = format_data_from_nodes(desired_results)
|
|
|
|
if filtering:
|
|
if len(output):
|
|
extracted_entities_data[entity_id] = output
|
|
else:
|
|
extracted_entities_data[entity_id] = output
|
|
|
|
return extracted_entities_data
|
|
|
|
|
|
def correct_for_export(all_data):
|
|
"""
|
|
Process the data for spreadsheet export.
|
|
"""
|
|
for d in all_data:
|
|
for k, v in d.items():
|
|
if isinstance(v, list) or isinstance(v, tuple):
|
|
if len(v):
|
|
new_list = []
|
|
for data in v:
|
|
new_list.append(str(data))
|
|
d[k] = ','.join(new_list)
|
|
if len(v) == 0:
|
|
d[k] = 0
|
|
|
|
elif isinstance(v, ifcopenshell.entity_instance):
|
|
d[k] = v[0]
|
|
return all_data
|
|
|
|
|
|
def export_to_xlsx(xlsx_name, concepts, all_data):
|
|
"""
|
|
Export data towards XLSX spreadsheet format.
|
|
|
|
:param xlsx_name: Name of the outputted file.
|
|
:param concepts: List of mvdXML Concept instances.
|
|
:param all_data: Data extracted.
|
|
|
|
"""
|
|
|
|
if not os.path.isdir("spreadsheet_output/"):
|
|
os.mkdir("spreadsheet_output/")
|
|
|
|
workbook = xlsxwriter.Workbook("spreadsheet_output/" + xlsx_name)
|
|
worksheet = workbook.add_worksheet()
|
|
# Formats
|
|
bold_format = workbook.add_format()
|
|
bold_format.set_bold()
|
|
bold_format.set_center_across()
|
|
# Write first row
|
|
column_index = 0
|
|
for concept in concepts:
|
|
worksheet.write(0, column_index, concept.name, bold_format)
|
|
column_index += 1
|
|
|
|
col = 0
|
|
for feature in all_data:
|
|
row = 1
|
|
for d in feature.values():
|
|
worksheet.write(row, col, d)
|
|
row += 1
|
|
col += 1
|
|
|
|
workbook.close()
|
|
|
|
|
|
def export_to_csv(csv_name, concepts, all_data):
|
|
"""
|
|
Export data towards CSV spreadsheet format.
|
|
|
|
:param csv_name: Name of the file outputted file.
|
|
:param concepts: List of mvdXML Concept instances.
|
|
:param all_data: Data extracted.
|
|
"""
|
|
|
|
if not os.path.isdir("spreadsheet_output/"):
|
|
os.mkdir("spreadsheet_output/")
|
|
|
|
with open('spreadsheet_output/' + csv_name, 'w', newline='') as f:
|
|
writer = csv.writer(f)
|
|
header = [concept.name for concept in concepts]
|
|
first_row = writer.writerow(header)
|
|
|
|
values_by_row = []
|
|
for val in all_data:
|
|
values_by_row.append(list(val.values()))
|
|
entities_number = len(all_data[0].keys())
|
|
for i in range(0, entities_number):
|
|
row_to_write = []
|
|
for r in values_by_row:
|
|
row_to_write.append(r[i])
|
|
|
|
f = writer.writerow(row_to_write)
|
|
|
|
|
|
def get_data(mvd_concept, ifc_file, spreadsheet_export=True):
|
|
"""
|
|
Use the majority of all the other functions to return the data
|
|
queried by the mvdXML file in python format.
|
|
|
|
:param mvd_concept: mvdXML Concept instance.
|
|
:param ifc_file: IFC file from any schema.
|
|
:param spreadsheet_export: The spreadsheet export is carried out when set to True.
|
|
|
|
|
|
|
|
"""
|
|
|
|
# Check if IFC entities have been filtered at least once
|
|
filtered = 0
|
|
|
|
entities = ifc_file.by_type(mvd_concept.entity)
|
|
selected_entities = entities
|
|
verification_matrix = {}
|
|
for entity in selected_entities:
|
|
verification = dict()
|
|
verification_matrix[entity.GlobalId] = verification
|
|
|
|
# For each Concept(ConceptTemplate) in the ConceptRoot
|
|
concepts = sorted(mvd_concept.concepts(), key=is_applicability, reverse=True)
|
|
all_data = []
|
|
counter = 0
|
|
for concept in concepts:
|
|
if is_applicability(concept):
|
|
filtering = True
|
|
else:
|
|
filtering = False
|
|
|
|
# Access all the Rules of the ConceptTemplate
|
|
if len(concept.template().rules) > 1:
|
|
attribute_rules = []
|
|
for rule in concept.template().rules:
|
|
attribute_rules.append(rule)
|
|
rules_root = ifcopenshell.mvd.rule("EntityRule", mvd_concept.entity, attribute_rules)
|
|
else:
|
|
rules_root = concept.template().rules[0]
|
|
|
|
|
|
extracted_data = get_data_from_mvd(selected_entities, rules_root, filtering=filtering)
|
|
all_data.append(extracted_data)
|
|
|
|
if filtering:
|
|
filtered = 1
|
|
new_entities = []
|
|
for entity_id in all_data[counter].keys():
|
|
if len(all_data[counter][entity_id]) != 0:
|
|
entity = ifc_file.by_id(entity_id)
|
|
new_entities.append(entity)
|
|
|
|
selected_entities = new_entities
|
|
not_respecting_entities = [item for item in entities if item not in selected_entities]
|
|
for entity in entities:
|
|
val = 0
|
|
if entity in not_respecting_entities:
|
|
val = 1
|
|
verification_matrix[entity.GlobalId].update({concept.name: val})
|
|
counter += 1
|
|
|
|
all_data = correct_for_export(all_data)
|
|
|
|
if spreadsheet_export:
|
|
if filtered != 0:
|
|
export_name = "output_filtered"
|
|
else:
|
|
export_name = "output_non_filtered"
|
|
export_to_xlsx(export_name + '.xlsx', concepts, all_data)
|
|
export_to_csv(export_name + '.csv', concepts, all_data)
|
|
|
|
|
|
return all_data, verification_matrix
|
|
|
|
|
|
def get_non_respecting_entities(file, verification_matrix):
|
|
non_respecting = []
|
|
for k, v in verification_matrix.items():
|
|
entity = file.by_id(k)
|
|
print(list(v.values()))
|
|
if sum(v.values()) != 0:
|
|
non_respecting.append(entity)
|
|
|
|
return non_respecting
|
|
|
|
|
|
|
|
|
|
def get_respecting_entities(file, verification_matrix):
|
|
respecting = []
|
|
for k, v in verification_matrix.items():
|
|
entity = file.by_id(k)
|
|
print(list(v.values()))
|
|
if sum(v.values()) == 0:
|
|
respecting.append(entity)
|
|
|
|
return respecting
|
|
|
|
|
|
def visualize(file, not_respecting_entities):
|
|
"""
|
|
Visualize the instances of the entity type targeted by the mvdXML ConceptRoot.
|
|
At display, a color differentiation is made between the entities which comply with
|
|
mvdXML requirements and the ones which don't.
|
|
|
|
:param file: IFC file from any schema.
|
|
:param not_respecting_entities: Entities which don't comply with mvdXML requirements.
|
|
|
|
"""
|
|
|
|
s = ifcopenshell.geom.main.settings()
|
|
s.set(s.USE_PYTHON_OPENCASCADE, True)
|
|
s.set(s.DISABLE_OPENING_SUBTRACTIONS, False)
|
|
|
|
viewer = ifcopenshell.geom.utils.initialize_display()
|
|
|
|
entity_type = not_respecting_entities[0].is_a()
|
|
|
|
other_entities = [x for x in file.by_type("IfcBuildingElement") if x.is_a() != str(entity_type)]
|
|
|
|
set_of_entities = set(not_respecting_entities) | set(file.by_type(entity_type))
|
|
set_to_display = set_of_entities.union(set(other_entities))
|
|
|
|
for el in set_to_display:
|
|
if el in not_respecting_entities:
|
|
c = (1, 0, 0, 1)
|
|
elif el in other_entities:
|
|
c = (1, 1, 1, 0)
|
|
else:
|
|
c = (0, 1, 0.5, 1)
|
|
|
|
try:
|
|
shape = ifcopenshell.geom.create_shape(s, el)
|
|
# OCC.BRepTools.breptools_Write(shape.geometry, "test.brep")
|
|
ds = ifcopenshell.geom.utils.display_shape(shape, clr=c)
|
|
except:
|
|
pass
|
|
|
|
viewer.FitAll()
|
|
|
|
ifcopenshell.geom.utils.main_loop()
|
|
|
|
|
|
def validate_data(concept, data):
|
|
import io
|
|
import ast
|
|
import operator
|
|
from functools import reduce, partial
|
|
|
|
rules = [x[0] for x in concept.rules() if not isinstance(x, str)]
|
|
|
|
def transform_data(d):
|
|
"""
|
|
Transform dictionary keys from tree nodes to rule ids
|
|
"""
|
|
|
|
return {(k.parent if k.bind is None and k.parent.bind is not None else k).bind: v for k, v in d.items()}
|
|
|
|
|
|
def parse_mvdxml_token(v):
|
|
# @todo make more permissive and tolerant
|
|
return ast.literal_eval(v)
|
|
|
|
|
|
data = list(map(transform_data, data))
|
|
|
|
output = io.StringIO()
|
|
|
|
# https://stackoverflow.com/a/70227259
|
|
def operation_reduce(x, y):
|
|
"""
|
|
Takes alternating value and function as input and
|
|
reduces while applying function
|
|
"""
|
|
|
|
if callable(x):
|
|
return x(y)
|
|
else:
|
|
return partial(y, x)
|
|
|
|
|
|
def apply_rules():
|
|
|
|
for r in rules:
|
|
|
|
def apply_data():
|
|
|
|
for d in data:
|
|
|
|
def translate(v):
|
|
if isinstance(v, str):
|
|
return getattr(operator, v.lower() + "_")
|
|
else:
|
|
if v.b == "Value":
|
|
return d.get(v.a) == parse_mvdxml_token(v.c)
|
|
elif v.b == "Type":
|
|
return d.get(v.a) and d.get(v.a).is_a(parse_mvdxml_token(v.c))
|
|
|
|
r2 = list(map(translate, r))
|
|
yield reduce(operation_reduce, r2)
|
|
|
|
v = any(list(apply_data()))
|
|
print(("Met:" if v else "Not met:"), r, file=output)
|
|
yield v
|
|
|
|
|
|
valid = all(list(apply_rules()))
|
|
return valid, output.getvalue()
|
|
|
|
|
|
if __name__ == '__main__':
|
|
print('functions to parse MVD rules and extract IFC data/filter IFC entities from them')
|