├── .gitignore ├── LICENSE ├── README.md ├── __init__.py ├── __main__.py ├── mvd.py ├── mvd_examples ├── Example-CV100.mvdxml ├── Example-CV104.mvdxml ├── Example-CV106.mvdxml ├── building.mvdxml ├── officials │ └── ReferenceView_V1-2.mvdxml ├── wall_extraction.mvdxml └── xset.mvdxml ├── mvdxml_expression.py └── sparql.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## python-mvdxml 2 | 3 | A mvdXML checker and w3c SPARQL converter, as an IfcOpenShell submodule or stand-alone. 4 | 5 | WARNING: While this repository has many useful building blocks to build software around mvdXML and IFC, there are many mvdXML dialects and not all variants are likely to be fully supported. 6 | 7 | ### Quickstart 8 | 9 | #### Extraction 10 | 11 | ```python 12 | import ifcopenshell 13 | from ifcopenshell.mvd import mvd 14 | 15 | mvd_concept = mvd.open_mvd("examples/wall_extraction.mvdxml") 16 | file = ifcopenshell.open("Duplex_A_20110505.ifc") 17 | 18 | all_data = mvd.get_data(mvd_concept, file, spreadsheet_export=True) 19 | 20 | non_respecting_entities = mvd.get_non_respecting_entities(file, all_data[1]) 21 | respecting_entities = mvd.get_respecting_entities(file, all_data[1]) 22 | 23 | 24 | ``` 25 | 26 | ```python 27 | # Create a new file 28 | new_file = ifcopenshell.file(schema=file.schema) 29 | proj = file.by_type("IfcProject")[0] 30 | new_file.add(proj) 31 | 32 | for e in respecting_entities: 33 | new_file.add(e) 34 | 35 | new_file.write("new_file.ifc") 36 | ``` 37 | 38 | ```python 39 | # Visualize results 40 | mvd.visualize(file, non_respecting_entities) 41 | ``` 42 | 43 | ##### Validation 44 | 45 | ~~~py 46 | import ifcopenshell 47 | 48 | from ifcopenshell.mvd import mvd 49 | from colorama import Fore 50 | from colorama import Style 51 | 52 | concept_roots = list(ifcopenshell.mvd.concept_root.parse(MVDXML_FILENAME)) 53 | file = ifcopenshell.open(IFC_FILENAME) 54 | 55 | tt = 0 # total number of tests 56 | ts = 0 # total number of successful tests 57 | 58 | for concept_root in concept_roots: 59 | print("ConceptRoot: ", concept_root.entity) 60 | for concept in concept_root.concepts(): 61 | tt = tt + 1 62 | print("Concept: ", concept.name) 63 | try: 64 | 65 | if len(concept.template().rules) > 1: 66 | attribute_rules = [] 67 | for rule in concept.template().rules: 68 | attribute_rules.append(rule) 69 | rules_root = ifcopenshell.mvd.rule("EntityRule", concept_root.entity, attribute_rules) 70 | else: 71 | rules_root = concept.template().rules[0] 72 | ts = ts + 1 73 | finst = 0 #failed instances 74 | 75 | for inst in file.by_type(concept_root.entity): 76 | try: 77 | data = mvd.extract_data(rules_root, inst) 78 | valid, output = mvd.validate_data(concept, data) 79 | if not valid: 80 | finst = finst + 1 81 | print("[VALID]" if valid else Fore.RED +"[failure]"+Style.RESET_ALL, inst) 82 | print(output) 83 | except Exception as e: 84 | print(Fore.RED+"EXCEPTION: ", e, Style.RESET_ALL,inst) 85 | print () 86 | print (int(finst), "out of", int(len(file.by_type(concept_root.entity))), "instances failed the check") 87 | print ("---------------------------------") 88 | except Exception as e: 89 | print("EXCEPTION: "+Fore.RED,e,Style.RESET_ALL) 90 | print("---------------------------------") 91 | print("---------------------------------") 92 | print("---------------------------------") 93 | 94 | tf = tt-ts # total number of failed tests 95 | 96 | print ("\nRESULTS OVERVIEW") 97 | print ("Total number of tests: ",tt) 98 | print ("Total number of executed tests: ", ts) 99 | print ("Total number of failed tests: ", tf) 100 | ~~~ 101 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | from . import mvdxml_expression 2 | 3 | from xml.dom.minidom import parse, Element 4 | 5 | class rule(object): 6 | """ 7 | A class for representing an mvdXML EntityRule or AttributeRule 8 | """ 9 | parent = None 10 | 11 | def __init__(self, tag, attribute, nodes, bind=None, optional=False): 12 | self.tag, self.attribute, self.nodes, self.bind = tag, attribute, nodes, bind 13 | self.optional = optional 14 | 15 | def to_string(self, indent=0): 16 | # return "%s%s%s[%s](%s%s)%s" % ("\n" if indent else "", " "*indent, self.tag, self.attribute, "".join(n.to_string(indent+2) for n in self.nodes), ("\n" + " "*indent) if len(self.nodes) else "", (" -> %s" % self.bind) if self.bind else "") 17 | return "<%s %s%s>" % (self.tag, f"{self.bind}=" if self.bind else "", self.attribute) 18 | 19 | def __repr__(self): 20 | return self.to_string() 21 | 22 | class template(object): 23 | """ 24 | Representation of an mvdXML template 25 | """ 26 | 27 | def __init__(self, concept, root, constraints=None, rules=None, parent=None): 28 | self.concept, self.root, self.constraints, self.parent = concept, root, (constraints or []), parent 29 | self.rules = rules or [] 30 | self.entity = str(root.attributes['applicableEntity'].value) 31 | try: 32 | self.name = root.attributes['name'].value 33 | except: 34 | self.name = None 35 | 36 | def bind(self, constraints): 37 | return template(self.concept, self.root, constraints, self.rules) 38 | 39 | def parse(self, visited=None): 40 | for rules in self.root.getElementsByTagNameNS("*", "Rules"): 41 | for r in rules.childNodes: 42 | if not isinstance(r, Element): continue 43 | self.rules.append(self.parse_rule(r, visited=visited)) 44 | 45 | def traverse(self, fn, root=None, with_parents=False): 46 | def visit(n, p=root, ps=[root]): 47 | if with_parents: 48 | close = fn(rule=n, parents=ps) 49 | else: 50 | close = fn(rule=n, parent=p) 51 | 52 | for s in n.nodes: 53 | visit(s, n, ps + [n]) 54 | 55 | if close: 56 | close() 57 | 58 | for r in self.rules: 59 | visit(r) 60 | 61 | def parse_rule(self, root, visited=None): 62 | def visit(node, prefix="", visited=None, parent=None): 63 | r = None 64 | n = node 65 | nm = None 66 | p = prefix 67 | optional = False 68 | visited = set() if visited is None else visited 69 | 70 | if node.localName == "AttributeRule": 71 | r = node.attributes["AttributeName"].value 72 | try: 73 | nm = node.attributes["RuleID"].value 74 | except: 75 | # without binding, it's wrapped in a SPARQL OPTIONAL {} clause 76 | # Aim is to insert this clause once as high in the stack as possible 77 | # All topmost attribute rules are optional anyway as in the binding requirements on existence is specified 78 | 79 | def child_has_ruleid_or_prefix(node): 80 | if type(node).__name__ == "Element": 81 | if "RuleID" in node.attributes or "IdPrefix" in node.attributes: 82 | return True 83 | for n in node.childNodes: 84 | if child_has_ruleid_or_prefix(n): return True 85 | 86 | optional = node.parentNode.localName == "Rules" or not child_has_ruleid_or_prefix(node) 87 | elif node.localName == "EntityRule": 88 | r = node.attributes["EntityName"].value 89 | elif node.localName == "Template": 90 | ref = node.attributes['ref'].value 91 | # we break infinite recursion using this set 92 | if ref not in visited: 93 | n = self.concept.template(ref, visited=visited | {ref}).root 94 | try: 95 | p = p + node.attributes["IdPrefix"].value 96 | except: 97 | pass 98 | elif node.localName == "Constraint": 99 | r = mvdxml_expression.parse(node.attributes["Expression"].value) 100 | elif node.localName == "EntityRules": pass 101 | elif node.localName == "AttributeRules": pass 102 | elif node.localName == "Rules": pass 103 | elif node.localName == "Constraints": pass 104 | elif node.localName == "References": pass 105 | elif node.localName == "Definitions": return 106 | elif node.localName == "SubTemplates": return # @todo perhaps just traverse them? 107 | else: 108 | raise ValueError(node.localName) 109 | 110 | def _(n): 111 | for subnode in n.childNodes: 112 | if not isinstance(subnode, Element): continue 113 | for x in visit(subnode, p, visited=visited): yield x 114 | 115 | if r: 116 | R = rule(node.localName, r, list(_(n)), (p + nm) if nm else nm, optional=optional) 117 | for rr in R.nodes: 118 | rr.parent = R 119 | yield R 120 | else: 121 | for subnode in n.childNodes: 122 | if not isinstance(subnode, Element): continue 123 | for x in visit(subnode, p, visited=visited): yield x 124 | 125 | return list(visit(root, visited=visited))[0] 126 | 127 | class concept_or_applicability(object): 128 | """ 129 | Representation of either a mvdXML Concept or the Applicability node. Basically a structure 130 | for the hierarchical TemplateRule 131 | """ 132 | 133 | def __init__(self, root, c): 134 | self.root = root 135 | self.concept_node = c 136 | try: 137 | self.name = c.attributes["name"].value 138 | except: 139 | # probably applicability and not concept 140 | self.name = "Applicability" 141 | 142 | def template(self, id=None, visited=None): 143 | if id is None: 144 | id = self.concept_node.getElementsByTagNameNS("*","Template")[0].attributes['ref'].value 145 | 146 | for node in self.root.dom.getElementsByTagNameNS('*',"ConceptTemplate"): 147 | if node.attributes["uuid"].value == id: 148 | t = template(self, node) 149 | t.parse(visited=visited) 150 | t_with_rules = t.bind(self.rules()) 151 | return t_with_rules 152 | 153 | def rules(self): 154 | # Get the top most TemplateRule and traverse 155 | try: 156 | rules = self.concept_node.getElementsByTagNameNS("*","TemplateRules")[0] 157 | except: 158 | return [] 159 | 160 | def visit(rules): 161 | def _(): 162 | for i, r in enumerate([c for c in rules.childNodes if isinstance(c, Element)]): 163 | if i: 164 | yield rules.attributes["operator"].value 165 | if r.localName == "TemplateRules": 166 | yield visit(r) 167 | elif r.localName == "TemplateRule": 168 | yield mvdxml_expression.parse(r.attributes["Parameters"].value) 169 | else: 170 | raise Exception() 171 | 172 | return list(_()) 173 | 174 | return visit(rules) 175 | 176 | class concept_root(object): 177 | def __init__(self, dom, root): 178 | self.dom, self.root = dom, root 179 | self.name = root.attributes['name'].value 180 | self.entity = str(root.attributes['applicableRootEntity'].value) 181 | 182 | def applicability(self): 183 | return concept_or_applicability(self, self.root.getElementsByTagNameNS("*","Applicability")[0]) 184 | 185 | def concepts(self): 186 | for c in self.root.getElementsByTagNameNS("*","Concept"): 187 | yield concept_or_applicability(self, c) 188 | 189 | @staticmethod 190 | def parse(fn): 191 | dom = parse(fn) 192 | if len(dom.getElementsByTagNameNS("*","ConceptRoot")): 193 | for root in dom.getElementsByTagNameNS("*","ConceptRoot"): 194 | CR = concept_root(dom, root) 195 | yield CR 196 | else: 197 | for templ in dom.getElementsByTagNameNS("*","ConceptTemplate"): 198 | t = template(None, templ) 199 | t.parse() 200 | yield t 201 | -------------------------------------------------------------------------------- /__main__.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | if __name__ == "__main__": 4 | import sys 5 | from . import concept_root 6 | 7 | if len(sys.argv) == 2: 8 | mvdfn = sys.argv[1] 9 | for mvd in concept_root.parse(mvdfn): 10 | 11 | def dump(rule, parents): 12 | print(" " * len(parents), rule.tag, rule.attribute) 13 | 14 | for c in mvd.concepts(): 15 | print(c.name) 16 | print() 17 | 18 | t = c.template() 19 | print("RootEntity", t.entity) 20 | t.traverse(dump, with_parents=True) 21 | print(" ".join(map(str, t.constraints))) 22 | 23 | print() 24 | 25 | elif len(sys.argv) == 3: 26 | from . import sparql 27 | mvdfn,ttlfn = sys.argv[1:] 28 | sparql.derive_prefix(ttlfn) 29 | ttlfn = sparql.infer_subtypes(ttlfn) 30 | for mvd in concept_root.parse(mvdfn): 31 | sparql.executor.run(mvd, mvdfn, ttlfn) 32 | 33 | else: 34 | print(sys.executable, "ifcopenshell.mvd", "<.mvdxml>") 35 | print(sys.executable, "ifcopenshell.mvd", "<.mvdxml>", "<.ifc>") 36 | -------------------------------------------------------------------------------- /mvd.py: -------------------------------------------------------------------------------- 1 | import ifcopenshell 2 | import ifcopenshell.geom 3 | 4 | import os 5 | import itertools 6 | 7 | import csv 8 | import xlsxwriter 9 | 10 | 11 | def is_applicability(concept): 12 | """ 13 | Check whether the Concept created has a filtering purpose. 14 | Actually, MvdXML has a specific Applicability node. 15 | 16 | :param concept: mvdXML Concept object 17 | """ 18 | return concept.name.startswith("AP") 19 | 20 | 21 | def merge_dictionaries(dicts): 22 | d = {} 23 | for e in dicts: 24 | d.update(e) 25 | return d 26 | 27 | 28 | def extract_data(mvd_node, ifc_data): 29 | """ 30 | Recursively traverses mvdXML Concept tree structure. 31 | This tree is made of different mvdXML Rule nodes: AttributesRule 32 | and EntityRule. 33 | 34 | :param mvd_node: an mvdXML Concept 35 | :param ifc_data: an IFC instance or an IFC value 36 | 37 | 38 | """ 39 | to_combine = [] 40 | return_value = [] 41 | 42 | if len(mvd_node.nodes) == 0: 43 | if mvd_node.tag == "AttributeRule": 44 | try: 45 | values_from_attribute = getattr(ifc_data, mvd_node.attribute) 46 | return [{mvd_node: values_from_attribute}] 47 | except: 48 | return [{mvd_node: "Invalid Attribute"}] 49 | 50 | else: 51 | return [{mvd_node: ifc_data}] 52 | 53 | if mvd_node.tag == 'AttributeRule': 54 | data_from_attribute = [] 55 | try: 56 | values_from_attribute = getattr(ifc_data, mvd_node.attribute) 57 | if values_from_attribute is None: 58 | return [{mvd_node:"Nonexistent value"}] 59 | 60 | except: 61 | return [{mvd_node:"Invalid attribute rule"}] 62 | 63 | 64 | if isinstance(values_from_attribute, (list, tuple)): 65 | if len(values_from_attribute) == 0: 66 | return [{mvd_node: 'empty data structure'}] 67 | data_from_attribute.extend(values_from_attribute) 68 | 69 | else: 70 | data_from_attribute.append(values_from_attribute) 71 | 72 | for child in mvd_node.nodes: 73 | for data in data_from_attribute: 74 | child_values = extract_data(child, data) 75 | if isinstance(child_values, (list, tuple)): 76 | return_value.extend(child_values) 77 | else: 78 | return_value.append(child_values) 79 | return return_value 80 | 81 | elif mvd_node.tag == 'EntityRule': 82 | # Avoid things like Quantities on Psets 83 | if len(mvd_node.nodes): 84 | if isinstance(ifc_data, ifcopenshell.entity_instance) and not ifc_data.is_a(mvd_node.attribute): 85 | return [] 86 | 87 | for child in mvd_node.nodes: 88 | if child.tag == "Constraint": 89 | on_node = child.attribute[0].c 90 | on_node = on_node.replace("'", "") 91 | if isinstance(ifc_data, ifcopenshell.entity_instance): 92 | ifc_type = type(ifc_data[0]) 93 | typed_node = (ifc_type)(on_node) 94 | 95 | if ifc_data[0] == typed_node: 96 | return [{mvd_node: ifc_data}] 97 | 98 | elif ifc_data == on_node: 99 | return [{mvd_node: ifc_data}] 100 | else: 101 | to_combine.append(extract_data(child, ifc_data)) 102 | 103 | if len(to_combine): 104 | return_value = list(map(merge_dictionaries, itertools.product(*to_combine))) 105 | 106 | return return_value 107 | 108 | 109 | def open_mvd(filename): 110 | """ 111 | Open an mvdXML file. 112 | 113 | :param filename: Path of the mvdXML file. 114 | :return: mvdXML Concept instance. 115 | """ 116 | my_concept_object = list(ifcopenshell.mvd.concept_root.parse(filename))[0] 117 | return my_concept_object 118 | 119 | 120 | def format_data_from_nodes(recurse_output): 121 | """ 122 | Enable to format data collected such that the value to be exported is extracted. 123 | 124 | :param recurse_output: Data extracted from the recursive function 125 | 126 | """ 127 | if len(recurse_output) > 1: 128 | output = [] 129 | for resulting_dict in recurse_output: 130 | intermediate_storing = [] 131 | for value in resulting_dict.values(): 132 | intermediate_storing.append(value) 133 | output.extend(intermediate_storing) 134 | return output 135 | 136 | elif len(recurse_output) == 1: 137 | return_list = [] 138 | intermediate_list = list(recurse_output[0].values()) 139 | if len(intermediate_list) > 1: 140 | returned_value = intermediate_list 141 | for element in intermediate_list: 142 | # In case of a property that comes with all its path 143 | # (like ['PSet_WallCommon, 'IsExternal', IfcBoolean(.F.) 144 | # return only the list element which is not of string type 145 | # todo: check above condition with ifcopenshell type 146 | if not isinstance(element, str): 147 | returned_value = element 148 | if returned_value != intermediate_list: 149 | return returned_value 150 | else: 151 | return intermediate_list 152 | else: 153 | return intermediate_list[0] 154 | 155 | else: 156 | return [] 157 | 158 | 159 | def get_data_from_mvd(entities, tree, filtering=False): 160 | """ 161 | Apply the recursive function on the entities to return 162 | the values extracted. 163 | 164 | :param entities: IFC instances to be processed. 165 | :param tree: mvdXML Concept instance tree root. 166 | :param filtering: Indicates whether the mvdXML tree is an applicability. 167 | 168 | """ 169 | filtered_entities = [] 170 | extracted_entities_data = {} 171 | 172 | for entity in entities: 173 | entity_id = entity.GlobalId 174 | combinations = extract_data(tree, entity) 175 | desired_results = [] 176 | 177 | for dictionary in combinations: 178 | desired_results.append(dictionary) 179 | 180 | output = format_data_from_nodes(desired_results) 181 | 182 | if filtering: 183 | if len(output): 184 | extracted_entities_data[entity_id] = output 185 | else: 186 | extracted_entities_data[entity_id] = output 187 | 188 | return extracted_entities_data 189 | 190 | 191 | def correct_for_export(all_data): 192 | """ 193 | Process the data for spreadsheet export. 194 | """ 195 | for d in all_data: 196 | for k, v in d.items(): 197 | if isinstance(v, list) or isinstance(v, tuple): 198 | if len(v): 199 | new_list = [] 200 | for data in v: 201 | new_list.append(str(data)) 202 | d[k] = ','.join(new_list) 203 | if len(v) == 0: 204 | d[k] = 0 205 | 206 | elif isinstance(v, ifcopenshell.entity_instance): 207 | d[k] = v[0] 208 | return all_data 209 | 210 | 211 | def export_to_xlsx(xlsx_name, concepts, all_data): 212 | """ 213 | Export data towards XLSX spreadsheet format. 214 | 215 | :param xlsx_name: Name of the outputted file. 216 | :param concepts: List of mvdXML Concept instances. 217 | :param all_data: Data extracted. 218 | 219 | """ 220 | 221 | if not os.path.isdir("spreadsheet_output/"): 222 | os.mkdir("spreadsheet_output/") 223 | 224 | workbook = xlsxwriter.Workbook("spreadsheet_output/" + xlsx_name) 225 | worksheet = workbook.add_worksheet() 226 | # Formats 227 | bold_format = workbook.add_format() 228 | bold_format.set_bold() 229 | bold_format.set_center_across() 230 | # Write first row 231 | column_index = 0 232 | for concept in concepts: 233 | worksheet.write(0, column_index, concept.name, bold_format) 234 | column_index += 1 235 | 236 | col = 0 237 | for feature in all_data: 238 | row = 1 239 | for d in feature.values(): 240 | worksheet.write(row, col, d) 241 | row += 1 242 | col += 1 243 | 244 | workbook.close() 245 | 246 | 247 | def export_to_csv(csv_name, concepts, all_data): 248 | """ 249 | Export data towards CSV spreadsheet format. 250 | 251 | :param csv_name: Name of the file outputted file. 252 | :param concepts: List of mvdXML Concept instances. 253 | :param all_data: Data extracted. 254 | """ 255 | 256 | if not os.path.isdir("spreadsheet_output/"): 257 | os.mkdir("spreadsheet_output/") 258 | 259 | with open('spreadsheet_output/' + csv_name, 'w', newline='') as f: 260 | writer = csv.writer(f) 261 | header = [concept.name for concept in concepts] 262 | first_row = writer.writerow(header) 263 | 264 | values_by_row = [] 265 | for val in all_data: 266 | values_by_row.append(list(val.values())) 267 | entities_number = len(all_data[0].keys()) 268 | for i in range(0, entities_number): 269 | row_to_write = [] 270 | for r in values_by_row: 271 | row_to_write.append(r[i]) 272 | 273 | f = writer.writerow(row_to_write) 274 | 275 | 276 | def get_data(mvd_concept, ifc_file, spreadsheet_export=True): 277 | """ 278 | Use the majority of all the other functions to return the data 279 | queried by the mvdXML file in python format. 280 | 281 | :param mvd_concept: mvdXML Concept instance. 282 | :param ifc_file: IFC file from any schema. 283 | :param spreadsheet_export: The spreadsheet export is carried out when set to True. 284 | 285 | 286 | 287 | """ 288 | 289 | # Check if IFC entities have been filtered at least once 290 | filtered = 0 291 | 292 | entities = ifc_file.by_type(mvd_concept.entity) 293 | selected_entities = entities 294 | verification_matrix = {} 295 | for entity in selected_entities: 296 | verification = dict() 297 | verification_matrix[entity.GlobalId] = verification 298 | 299 | # For each Concept(ConceptTemplate) in the ConceptRoot 300 | concepts = sorted(mvd_concept.concepts(), key=is_applicability, reverse=True) 301 | all_data = [] 302 | counter = 0 303 | for concept in concepts: 304 | if is_applicability(concept): 305 | filtering = True 306 | else: 307 | filtering = False 308 | 309 | # Access all the Rules of the ConceptTemplate 310 | if len(concept.template().rules) > 1: 311 | attribute_rules = [] 312 | for rule in concept.template().rules: 313 | attribute_rules.append(rule) 314 | rules_root = ifcopenshell.mvd.rule("EntityRule", mvd_concept.entity, attribute_rules) 315 | else: 316 | rules_root = concept.template().rules[0] 317 | 318 | 319 | extracted_data = get_data_from_mvd(selected_entities, rules_root, filtering=filtering) 320 | all_data.append(extracted_data) 321 | 322 | if filtering: 323 | filtered = 1 324 | new_entities = [] 325 | for entity_id in all_data[counter].keys(): 326 | if len(all_data[counter][entity_id]) != 0: 327 | entity = ifc_file.by_id(entity_id) 328 | new_entities.append(entity) 329 | 330 | selected_entities = new_entities 331 | not_respecting_entities = [item for item in entities if item not in selected_entities] 332 | for entity in entities: 333 | val = 0 334 | if entity in not_respecting_entities: 335 | val = 1 336 | verification_matrix[entity.GlobalId].update({concept.name: val}) 337 | counter += 1 338 | 339 | all_data = correct_for_export(all_data) 340 | 341 | if spreadsheet_export: 342 | if filtered != 0: 343 | export_name = "output_filtered" 344 | else: 345 | export_name = "output_non_filtered" 346 | export_to_xlsx(export_name + '.xlsx', concepts, all_data) 347 | export_to_csv(export_name + '.csv', concepts, all_data) 348 | 349 | 350 | return all_data, verification_matrix 351 | 352 | 353 | def get_non_respecting_entities(file, verification_matrix): 354 | non_respecting = [] 355 | for k, v in verification_matrix.items(): 356 | entity = file.by_id(k) 357 | print(list(v.values())) 358 | if sum(v.values()) != 0: 359 | non_respecting.append(entity) 360 | 361 | return non_respecting 362 | 363 | 364 | 365 | 366 | def get_respecting_entities(file, verification_matrix): 367 | respecting = [] 368 | for k, v in verification_matrix.items(): 369 | entity = file.by_id(k) 370 | print(list(v.values())) 371 | if sum(v.values()) == 0: 372 | respecting.append(entity) 373 | 374 | return respecting 375 | 376 | 377 | def visualize(file, not_respecting_entities): 378 | """ 379 | Visualize the instances of the entity type targeted by the mvdXML ConceptRoot. 380 | At display, a color differentiation is made between the entities which comply with 381 | mvdXML requirements and the ones which don't. 382 | 383 | :param file: IFC file from any schema. 384 | :param not_respecting_entities: Entities which don't comply with mvdXML requirements. 385 | 386 | """ 387 | 388 | s = ifcopenshell.geom.main.settings() 389 | s.set(s.USE_PYTHON_OPENCASCADE, True) 390 | s.set(s.DISABLE_OPENING_SUBTRACTIONS, False) 391 | 392 | viewer = ifcopenshell.geom.utils.initialize_display() 393 | 394 | entity_type = not_respecting_entities[0].is_a() 395 | 396 | other_entities = [x for x in file.by_type("IfcBuildingElement") if x.is_a() != str(entity_type)] 397 | 398 | set_of_entities = set(not_respecting_entities) | set(file.by_type(entity_type)) 399 | set_to_display = set_of_entities.union(set(other_entities)) 400 | 401 | for el in set_to_display: 402 | if el in not_respecting_entities: 403 | c = (1, 0, 0, 1) 404 | elif el in other_entities: 405 | c = (1, 1, 1, 0) 406 | else: 407 | c = (0, 1, 0.5, 1) 408 | 409 | try: 410 | shape = ifcopenshell.geom.create_shape(s, el) 411 | # OCC.BRepTools.breptools_Write(shape.geometry, "test.brep") 412 | ds = ifcopenshell.geom.utils.display_shape(shape, clr=c) 413 | except: 414 | pass 415 | 416 | viewer.FitAll() 417 | 418 | ifcopenshell.geom.utils.main_loop() 419 | 420 | 421 | def validate_data(concept, data): 422 | import io 423 | import ast 424 | import operator 425 | from functools import reduce, partial 426 | 427 | rules = [x[0] for x in concept.rules() if not isinstance(x, str)] 428 | 429 | def transform_data(d): 430 | """ 431 | Transform dictionary keys from tree nodes to rule ids 432 | """ 433 | 434 | return {(k.parent if k.bind is None and (k.parent is not None and k.parent.bind is not None) else k).bind: v for k, v in d.items()} 435 | 436 | 437 | def parse_mvdxml_token(v): 438 | if v.lower() == "true": 439 | return True 440 | if v.lower() == "false": 441 | return False 442 | # @todo make more permissive and tolerant 443 | return ast.literal_eval(v) 444 | 445 | 446 | data = list(map(transform_data, data)) 447 | 448 | output = io.StringIO() 449 | 450 | # https://stackoverflow.com/a/70227259 451 | def operation_reduce(x, y): 452 | """ 453 | Takes alternating value and function as input and 454 | reduces while applying function 455 | """ 456 | 457 | if callable(x): 458 | return x(y) 459 | else: 460 | return partial(y, x) 461 | 462 | 463 | def apply_rules(): 464 | 465 | for r in rules: 466 | 467 | def apply_data(): 468 | 469 | for d in data: 470 | 471 | def translate(v): 472 | if isinstance(v, str): 473 | return getattr(operator, v.lower() + "_") 474 | else: 475 | if v.b == "Value" or v.b is None: 476 | return d.get(v.a) == parse_mvdxml_token(v.c) 477 | elif v.b == "Type": 478 | return d.get(v.a) is not None and d.get(v.a).is_a(parse_mvdxml_token(v.c)) 479 | elif v.b == "Exists": 480 | return (d.get(v.a) is not None) == parse_mvdxml_token(v.c) 481 | else: 482 | raise RuntimeError(f"Invalid rule predicate {v.b}") 483 | 484 | r2 = list(map(translate, r)) 485 | yield reduce(operation_reduce, r2) 486 | 487 | v = any(list(apply_data())) 488 | print(("Met:" if v else "Not met:"), r, file=output) 489 | yield v 490 | 491 | 492 | valid = all(list(apply_rules())) 493 | return valid, output.getvalue() 494 | 495 | 496 | if __name__ == '__main__': 497 | print('functions to parse MVD rules and extract IFC data/filter IFC entities from them') 498 | -------------------------------------------------------------------------------- /mvd_examples/Example-CV100.mvdxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | Any product or product type can have associated materials indicating the physical composition of an object. 17 | 18 | Materials can have representations for surface styles indicating colors, textures, and light reflectance for 3D 19 | 20 | rendering. Materials can have representations for fill styles indicating colors, tiles, and hatch patterns for 21 | 22 | 2D rendering. Materials can have properties such as density, elasticity, thermal resistance, and others as 23 | 24 | defined in this specification. Materials can also be classified according to a referenced industry standard. 25 | 26 |

27 | 28 |

29 | 30 | An object can be comprised of a single material or a set of materials with a particular layout. Several 31 | 32 | examples include: 33 | 34 |

35 | 36 |
    37 | 38 |
  • a slab may have an associated layer of concrete; 39 | 40 |
  • 41 | 42 |
  • a beam may have an associated I-Shape profile of steel; 43 | 44 |
  • 45 | 46 |
  • a door may have associated constituents for framing and glazing; 47 | 48 |
  • 49 | 50 |
  • a port may have an associated profile and/or material flowing through it such as hot water. 51 | 52 |
  • 53 | 54 |
55 | ]]> 56 |
57 |
58 | 59 | 60 | 61 | 62 | Material layer set usage defines layout at occurrences to indicate a direction and offset from the 'Axis' reference curve, and a reference extent such as for a default wall height.

]]> 63 |
64 |
65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 |
162 |
163 |
164 |
165 |
166 |
167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 |