diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 84cab6dd9f..6601b2dce6 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -238,6 +238,18 @@ PluginOpenFOAM:py3.11: variables: plugin: "PluginOpenFOAM" +PluginIFCCheck:py3.10: + <<: *test_template_plugin_integration + image: $CI_REGISTRY/bim2sim:py3.10-occ7.9.0 + variables: + plugin: "PluginIFCCheck" + +PluginIFCCheck:py3.11: + <<: *test_template_plugin_integration + image: $CI_REGISTRY/bim2sim:py3.11-occ7.9.0 + variables: + plugin: "PluginIFCCheck" + # Regression tests PluginTEASER_reg:py3.10: <<: *test_template_plugin_regression @@ -287,3 +299,14 @@ PluginOpenFOAM_reg:py3.11: variables: plugin: "PluginOpenFOAM" +PluginIFCCheck_reg:py3.10: + <<: *test_template_plugin_regression + image: $CI_REGISTRY/bim2sim:py3.10-occ7.9.0 + variables: + plugin: "PluginIFCCheck" + +PluginIFCCheck_reg:py3.11: + <<: *test_template_plugin_regression + image: $CI_REGISTRY/bim2sim:py3.11-occ7.9.0 + variables: + plugin: "PluginIFCCheck" diff --git a/.gitmodules b/.gitmodules index 1d866e7dc5..d2ca74073d 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,7 +1,7 @@ [submodule "test/resources"] path = test/resources url = https://github.com/BIM2SIM/bim2sim-test-resources.git - branch = main + branch = 15-update_test_resources_ifccheck [submodule "butterfly"] path = butterfly url = https://github.com/BIM2SIM/butterfly.git diff --git a/bim2sim/assets/templates/check_ifc/guid_template b/bim2sim/assets/templates/check_ifc/guid_template new file mode 100644 index 0000000000..5e3d991b22 --- /dev/null +++ b/bim2sim/assets/templates/check_ifc/guid_template @@ -0,0 +1,81 @@ + +

Overview

+ + + + + + + + + + + + + + + + + + + + + + + +
GUID error Summary
Non-Unique GUID +
Total non-unique GUIDs: ${len(double_guids)}
Empty GUID +
Total Empty GUIDs: ${len(empty_guids)}

+ +

Non-Unique GUIDs

+ + + + + + + + %for guid, values in double_guids.items(): + + + + + %endfor + +
GUIDName
${guid}${values.Name}

+ +

Empty GUIDs

+ + + + + + + + + %for key, values in empty_guids.items(): + + + + + + %endfor + +
Generated Key (bim2sim)NameGlobalID
${key}${values.Name}${values.GlobalId}

diff --git a/bim2sim/assets/templates/check_ifc/inst_template b/bim2sim/assets/templates/check_ifc/inst_template index 2dcda79e5c..af65bf9273 100644 --- a/bim2sim/assets/templates/check_ifc/inst_template +++ b/bim2sim/assets/templates/check_ifc/inst_template @@ -56,8 +56,7 @@ ${task.sub_inst_cls} - Total ${task.sub_inst_cls} with errors: -       + Total ${task.sub_inst_cls} with errors: ${len(task.error_summary_sub_inst)}/${len(task.sub_inst)} @@ -110,4 +109,4 @@ %endfor
-%endfor \ No newline at end of file +%endfor diff --git a/bim2sim/assets/templates/check_ifc/summary_template b/bim2sim/assets/templates/check_ifc/summary_template index 95ea9b1b07..10ca12e6c9 100644 --- a/bim2sim/assets/templates/check_ifc/summary_template +++ b/bim2sim/assets/templates/check_ifc/summary_template @@ -32,20 +32,20 @@ Total IFCProduct with errors: - ${len(task.error_summary_inst)}/${len(task.elements)} + ${len(task.error_summary_inst)}/${len(task.elements)} Total errors in IFCProducts: - ${sum(summary_inst['per_error'].values())} + ${sum(summary_inst['per_error'].values())} Total ${task.sub_inst_cls} with errors: - ${len(task.error_summary_sub_inst)}/${len(task.sub_inst)} + ${len(task.error_summary_sub_inst)}/${len(task.sub_inst)} Total errors in ${task.sub_inst_cls}: - ${sum(summary_sbs['per_error'].values())} + ${sum(summary_sbs['per_error'].values())} Total IFCProduct with missing properties: - ${len(task.error_summary_prop)}/${len(task.elements)} + ${len(task.error_summary_prop)}/${len(task.elements)} Total missing properties in IFCProducts: - ${sum(summary_props['per_error'].values())} + ${sum(summary_props['per_error'].values())} - \ No newline at end of file + diff --git a/bim2sim/assets/templates/check_ifc/summary_template_extend b/bim2sim/assets/templates/check_ifc/summary_template_extend new file mode 100644 index 0000000000..292024b05e --- /dev/null +++ b/bim2sim/assets/templates/check_ifc/summary_template_extend @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
${plugin_name} Error Summary
+ IFC Version +
IFC Version: ${ifc_version}
Version error: ${version_error}
+ GUID general +
Unique GUID: ${all_guids_unique}
Total non-unique GUIDs: ${len(double_guids)}
Empty GUID: ${all_guids_filled}
Total Empty GUIDs: ${len(empty_guids)}
+ Errors in IFC +
Total IFCProduct with errors: + ${len(task.error_summary_inst)}/${len(task.elements)}
Total errors in IFCProducts:${sum(summary_inst['per_error'].values())}
Total ${task.sub_inst_cls} with errors: + ${len(task.error_summary_sub_inst)}/${len(task.sub_inst)}
Total errors in ${task.sub_inst_cls}:${sum(summary_sbs['per_error'].values())}
Missing Attributes for + ${plugin_name} +
Total IFCProduct with missing properties: + ${len(task.error_summary_prop)}/${len(task.elements)}
Total missing properties in IFCProducts:${sum(summary_props['per_error'].values())}
diff --git a/bim2sim/elements/base_elements.py b/bim2sim/elements/base_elements.py index 8700f45cec..85e795b130 100644 --- a/bim2sim/elements/base_elements.py +++ b/bim2sim/elements/base_elements.py @@ -450,7 +450,7 @@ def __repr__(self): def __str__(self): return "%s" % self.__class__.__name__ - +# TODO remove one of both classes RelationBased (see above) class RelationBased(IFCBased): pass diff --git a/bim2sim/plugins/PluginAixLib/bim2sim_aixlib/__init__.py b/bim2sim/plugins/PluginAixLib/bim2sim_aixlib/__init__.py index ac1767d968..f81d423287 100644 --- a/bim2sim/plugins/PluginAixLib/bim2sim_aixlib/__init__.py +++ b/bim2sim/plugins/PluginAixLib/bim2sim_aixlib/__init__.py @@ -26,7 +26,7 @@ class PluginAixLib(Plugin): tasks = {LoadLibrariesAixLib} default_tasks = [ common.LoadIFC, - common.CheckIfc, + # common.CheckIfc, common.CreateElementsOnIfcTypes, hvac.ConnectElements, hvac.MakeGraph, diff --git a/bim2sim/plugins/PluginComfort/bim2sim_comfort/__init__.py b/bim2sim/plugins/PluginComfort/bim2sim_comfort/__init__.py index 88f84d76c3..eeed25fa75 100644 --- a/bim2sim/plugins/PluginComfort/bim2sim_comfort/__init__.py +++ b/bim2sim/plugins/PluginComfort/bim2sim_comfort/__init__.py @@ -23,7 +23,7 @@ class PluginComfort(Plugin): elements = {*bps_elements.items, Material} default_tasks = [ common.LoadIFC, - common.CheckIfc, + # common.CheckIfc, common.CreateElementsOnIfcTypes, bps.CreateSpaceBoundaries, bps.AddSpaceBoundaries2B, diff --git a/bim2sim/plugins/PluginEnergyPlus/bim2sim_energyplus/__init__.py b/bim2sim/plugins/PluginEnergyPlus/bim2sim_energyplus/__init__.py index c5f6181846..9883e11331 100644 --- a/bim2sim/plugins/PluginEnergyPlus/bim2sim_energyplus/__init__.py +++ b/bim2sim/plugins/PluginEnergyPlus/bim2sim_energyplus/__init__.py @@ -15,7 +15,7 @@ class PluginEnergyPlus(Plugin): sim_settings = EnergyPlusSimSettings default_tasks = [ common.LoadIFC, - common.CheckIfc, + # common.CheckIfc, common.CreateElementsOnIfcTypes, bps.CreateSpaceBoundaries, bps.AddSpaceBoundaries2B, diff --git a/bim2sim/plugins/PluginHKESim/bim2sim_hkesim/__init__.py b/bim2sim/plugins/PluginHKESim/bim2sim_hkesim/__init__.py index 9e2efb6460..1490fc1327 100644 --- a/bim2sim/plugins/PluginHKESim/bim2sim_hkesim/__init__.py +++ b/bim2sim/plugins/PluginHKESim/bim2sim_hkesim/__init__.py @@ -23,7 +23,7 @@ class PluginHKESim(Plugin): tasks = {LoadLibrariesHKESim} default_tasks = [ common.LoadIFC, - common.CheckIfc, + # common.CheckIfc, common.CreateElementsOnIfcTypes, hvac.ConnectElements, hvac.MakeGraph, diff --git a/bim2sim/plugins/PluginIFCCheck/__init__.py b/bim2sim/plugins/PluginIFCCheck/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/bim2sim/plugins/PluginIFCCheck/bim2sim_ifccheck/__init__.py b/bim2sim/plugins/PluginIFCCheck/bim2sim_ifccheck/__init__.py new file mode 100644 index 0000000000..e7216201ba --- /dev/null +++ b/bim2sim/plugins/PluginIFCCheck/bim2sim_ifccheck/__init__.py @@ -0,0 +1,19 @@ +"""Template plugin for bim2sim. + +Holds a plugin with only base tasks mostly for demonstration. +""" +from bim2sim.plugins import Plugin +from bim2sim.tasks import checks, common +from bim2sim.plugins.PluginIFCCheck.bim2sim_ifccheck.sim_settings import \ + CheckIFCSimSettings + + +class PluginIFCCheck(Plugin): + """PluginIFCCheck template.""" + + name = 'IFCCheck' + sim_settings = CheckIFCSimSettings + default_tasks = [ + common.LoadIFC, + checks.CheckIfc, + ] diff --git a/bim2sim/plugins/PluginIFCCheck/bim2sim_ifccheck/examples/e1_checkIFCIDS.ipynb b/bim2sim/plugins/PluginIFCCheck/bim2sim_ifccheck/examples/e1_checkIFCIDS.ipynb new file mode 100644 index 0000000000..3c9e216eee --- /dev/null +++ b/bim2sim/plugins/PluginIFCCheck/bim2sim_ifccheck/examples/e1_checkIFCIDS.ipynb @@ -0,0 +1,248 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "1f784365-841c-4eb6-beef-eab4d39027a4", + "metadata": {}, + "source": [ + "# example e1_checkIFCIDS_bps\n", + " \"\"\"Run a bim2sim project with the PluginIFCCheck\n", + "\n", + " This exmaple is used while the development of the ifc check based on IDS\n", + " task. After the development is finished, it can deleted or use as example\n", + " for the usage of this task.\n", + " \"\"\"\n" + ] + }, + { + "cell_type": "markdown", + "id": "315bf539-71cc-4509-9265-f048d793e5cb", + "metadata": {}, + "source": [ + "## code" + ] + }, + { + "cell_type": "markdown", + "id": "bd2c0579-d623-45c0-bde8-0c460febf321", + "metadata": {}, + "source": [ + "import needed libraries" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "9d889d34-e6bb-4a38-bfc4-2b0ceeef9e98", + "metadata": {}, + "outputs": [], + "source": [ + "import tempfile\n", + "from pathlib import Path\n", + "\n", + "import bim2sim\n", + "from bim2sim import Project, ConsoleDecisionHandler, run_project\n", + "from bim2sim.utilities.types import IFCDomain\n", + "from bim2sim.plugins.PluginIFCCheck.bim2sim_ifccheck import PluginIFCCheck\n" + ] + }, + { + "cell_type": "markdown", + "id": "8ec7c4c7-a88f-4360-8c0a-582b35a80515", + "metadata": {}, + "source": [ + "Create a temp directory for the project, feel free to use a \"normal\" directory" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "d302af61-2f01-44eb-9003-33d0ed9680b6", + "metadata": {}, + "outputs": [], + "source": [ + "project_path = Path(tempfile.TemporaryDirectory(\n", + " prefix='bim2sim_e1_checkifc').name)" + ] + }, + { + "cell_type": "markdown", + "id": "9b740382-ed23-41a1-88d3-2fb658d0d6d2", + "metadata": {}, + "source": [ + "Set the ifc path to use and define which domain the IFC belongs to" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "cd1dbc55-a8a6-4a80-9aa8-ec5b30883c43", + "metadata": {}, + "outputs": [], + "source": [ + "ifc_paths = {IFCDomain.arch:\n", + " Path(bim2sim.__file__).parent.parent /\n", + " 'test/resources/arch/ifc/AC20-FZK-Haus.ifc',\n", + " }" + ] + }, + { + "cell_type": "markdown", + "id": "36a29bb1-3fef-4e81-9777-b28cf011ef7a", + "metadata": {}, + "source": [ + "Create a project including the folder structure for the project with IFCCheck as backend" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "5294a886-685c-4efe-a8ab-8a190415b1ca", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[32;20m[DEV-INFO] - 2026-01-27 08:09:22,390 bim2sim.create: Project folder created.\u001b[0m\n", + "\u001b[32;20m[DEV-INFO] - 2026-01-27 08:09:22,391 bim2sim.kernel.decision.load: Unable to load decisions. No Existing decisions found at /tmp/bim2sim_e1_checkifceollbic4/decisions.json\u001b[0m\n", + "\u001b[32;20m[DEV-INFO] - 2026-01-27 08:09:22,391 bim2sim.sim_settings.update_from_config: Loaded 0 settings from config file.\u001b[0m\n" + ] + } + ], + "source": [ + "project = Project.create(project_path, ifc_paths, PluginIFCCheck)" + ] + }, + { + "cell_type": "markdown", + "id": "4274ed28-52e9-4e5b-a8aa-edd9cc2301e6", + "metadata": {}, + "source": [ + "set weather file data" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "c2c25e98-2445-4ead-97e8-3cdc2999af61", + "metadata": {}, + "outputs": [], + "source": [ + "project.sim_settings.weather_file_path = (Path(bim2sim.__file__).parent.parent /\n", + " 'test/resources/weather_files/DEU_NW_Aachen.105010_TMYx.mos')" + ] + }, + { + "cell_type": "markdown", + "id": "583ce276-1381-4880-86ea-75d068e6934c", + "metadata": {}, + "source": [ + "assign an IDS file, which is needed to check the ifc file by ifctester" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "cbf781a4-4280-4be2-8664-aa5c715e5bae", + "metadata": {}, + "outputs": [], + "source": [ + "project.sim_settings.ids_file_path = (Path(bim2sim.__file__).parent /\n", + " 'plugins/PluginIFCCheck/bim2sim_ifccheck/ifc_bps.ids')" + ] + }, + { + "cell_type": "markdown", + "id": "d2a1e7d4-6e86-4629-b938-5e2a9219ee46", + "metadata": {}, + "source": [ + "Run the project with the ConsoleDecisionHandler. This allows interactive input to answer upcoming questions regarding the imported IFC." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "5f6e5da0-e2ca-41d8-9ff0-be66dfb99979", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[32;20m[DEV-INFO] - 2026-01-27 08:09:22,405 bim2sim.Playground.run_task: Starting Task ''\u001b[0m\n", + "\u001b[32;20m[DEV-INFO] - 2026-01-27 08:09:22,406 bim2sim.tasks.base.LoadIFC.run: Loading IFC files\u001b[0m\n", + "\u001b[32;20m[DEV-INFO] - 2026-01-27 08:09:22,407 bim2sim.tasks.base.LoadIFC.load_ifc_files: Found 1 IFC files in project directory.\u001b[0m\n", + "\u001b[32;20m[DEV-INFO] - 2026-01-27 08:09:22,408 bim2sim.tasks.base.LoadIFC.load_ifc_files: Loading IFC file AC20-FZK-Haus.ifc 1/1.\u001b[0m\n", + "\u001b[32;20m[DEV-INFO] - 2026-01-27 08:09:22,408 bim2sim.load_ifc: Loading IFC AC20-FZK-Haus.ifc from /tmp/bim2sim_e1_checkifceollbic4/ifc/arch/AC20-FZK-Haus.ifc\u001b[0m\n", + "\u001b[32;20m[DEV-INFO] - 2026-01-27 08:09:22,631 bim2sim.kernel.ifc_file.get_ifc_units: Initializing units for IFC file: AC20-FZK-Haus.ifc\u001b[0m\n", + "\u001b[33;20m[DEV-WARNING] - 2026-01-27 08:09:22,659 bim2sim.kernel.ifc_file.get_ifc_units: Failed to parse #19=IfcConversionBasedUnit(#18,.PLANEANGLEUNIT.,'DEGREE',#17)\u001b[0m\n", + "\u001b[33;20m[DEV-WARNING] - 2026-01-27 08:09:22,661 bim2sim.kernel.ifc_file.get_ifc_units: Failed to parse #27=IfcDerivedUnit((#30,#32,#34),.THERMALCONDUCTANCEUNIT.,$)\u001b[0m\n", + "\u001b[33;20m[DEV-WARNING] - 2026-01-27 08:09:22,662 bim2sim.kernel.ifc_file.get_ifc_units: Failed to parse #35=IfcDerivedUnit((#38,#40,#42),.SPECIFICHEATCAPACITYUNIT.,$)\u001b[0m\n", + "\u001b[33;20m[DEV-WARNING] - 2026-01-27 08:09:22,663 bim2sim.kernel.ifc_file.get_ifc_units: Failed to parse #43=IfcDerivedUnit((#46,#48),.MASSDENSITYUNIT.,$)\u001b[0m\n", + "\u001b[32;20m[DEV-INFO] - 2026-01-27 08:09:22,665 bim2sim.elements.mapping.finder._set_templates_by_tools: Found matching template for IfcApplication withfull name ARCHICAD-64 in template ArchiCAD\u001b[0m\n", + "\u001b[32;20m[DEV-INFO] - 2026-01-27 08:09:22,666 bim2sim.tasks.base.LoadIFC.load_ifc_files: Loaded AC20-FZK-Haus.ifc for Domain arch. This took 0.26 seconds\u001b[0m\n", + "\u001b[32;20m[DEV-INFO] - 2026-01-27 08:09:22,667 bim2sim.tasks.base.LoadIFC.load_ifc_files: Loaded 1 IFC-files.\u001b[0m\n", + "\u001b[32;20m[DEV-INFO] - 2026-01-27 08:09:22,668 bim2sim.Playground.run_task: Successfully finished Task ''\u001b[0m\n", + "\u001b[32;20m[DEV-INFO] - 2026-01-27 08:09:22,668 bim2sim.Playground.run_task: done\u001b[0m\n", + "\u001b[32;20m[DEV-INFO] - 2026-01-27 08:09:22,669 bim2sim.Playground.run_task: Starting Task ''\u001b[0m\n", + "\u001b[32;20m[DEV-INFO] - 2026-01-27 08:09:22,670 bim2sim.tasks.base.CheckIfc.run: Processing IFC Checks with ifcTester\u001b[0m\n", + "\u001b[32;20m[DEV-INFO] - 2026-01-27 08:09:22,671 bim2sim.tasks.base.CheckIfc.run: Found 1 IFC files in project directory.\u001b[0m\n", + "\u001b[33;20m[DEV-WARNING] - 2026-01-27 08:09:23,201 bim2sim.tasks.base.CheckIfc.run: all checks of the specifications of this IDS pass: False\u001b[0m\n", + "\u001b[32;20m[DEV-INFO] - 2026-01-27 08:09:23,201 bim2sim.tasks.base.CheckIfc.run: Processing IFC Checks without ifcTester\u001b[0m\n", + "\u001b[32;20m[DEV-INFO] - 2026-01-27 08:09:23,578 bim2sim.tasks.base.CheckIfc.run: the GUIDs of all elements are unique: True\u001b[0m\n", + "\u001b[32;20m[DEV-INFO] - 2026-01-27 08:09:24,011 bim2sim.tasks.base.CheckIfc.run: the GUIDs of all elements are filled (NOT empty): True\u001b[0m\n", + "\u001b[32;20m[DEV-INFO] - 2026-01-27 08:09:24,012 bim2sim.tasks.base.CheckIfc.run: Processing BPS-IfcCheck\u001b[0m\n", + "\u001b[32;20m[DEV-INFO] - 2026-01-27 08:09:24,330 bim2sim.Playground.run_task: Successfully finished Task ''\u001b[0m\n", + "\u001b[32;20m[DEV-INFO] - 2026-01-27 08:09:24,331 bim2sim.Playground.run_task: done\u001b[0m\n", + "\u001b[32;20m[DEV-INFO] - 2026-01-27 08:09:24,332 bim2sim.kernel.decision.save: Saved 0 decisions.\u001b[0m\n", + "\u001b[32;20m[DEV-INFO] - 2026-01-27 08:09:24,333 bim2sim.finalize: Project Exports can be found under /tmp/bim2sim_e1_checkifceollbic4/export\u001b[0m\n", + "\u001b[32;20m[DEV-INFO] - 2026-01-27 08:09:24,333 bim2sim.finalize: Project \"AC20-FZK-Haus\" finished successful\u001b[0m\n" + ] + }, + { + "data": { + "text/plain": [ + "0" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "run_project(project, ConsoleDecisionHandler())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3ac1e1e0-32f7-4992-b86e-7d60aa5ae4ab", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "bim2sim3112025-2", + "language": "python", + "name": "bim2sim3112025-2" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/bim2sim/plugins/PluginIFCCheck/bim2sim_ifccheck/examples/e1_checkIFCIDS.py b/bim2sim/plugins/PluginIFCCheck/bim2sim_ifccheck/examples/e1_checkIFCIDS.py new file mode 100644 index 0000000000..8a850918a8 --- /dev/null +++ b/bim2sim/plugins/PluginIFCCheck/bim2sim_ifccheck/examples/e1_checkIFCIDS.py @@ -0,0 +1,58 @@ +"""Simple example for PluginIFCCheck.""" +import tempfile +from pathlib import Path + +import bim2sim +from bim2sim import Project, ConsoleDecisionHandler, run_project +from bim2sim.utilities.types import IFCDomain +from bim2sim.plugins.PluginIFCCheck.bim2sim_ifccheck import PluginIFCCheck + + +def run_simple_project(): + """Run a bim2sim project with the PluginIFCCheck.""" + # Create a temp directory for the project, feel free to use a "normal" + # directory + project_path = Path(tempfile.TemporaryDirectory( + prefix='bim2sim_e1_checkifc_').name) + + # Set the ifc path to use and define which domain the IFC belongs to. + # This is done via a dictionary, where the key is the domain and the value + # the path to the IFC file. We are using an architecture domain IFC file + # here from the FZK-Haus which is a simple IFC provided by KIT. + + ifc_paths = { + IFCDomain.arch: + Path(bim2sim.__file__).parent.parent / + 'test/resources/arch/ifc/AC20-FZK-Haus.ifc', + } + + project = Project.create( + project_path, ifc_paths, PluginIFCCheck) + + # Next to the plugin that should be used we can do further configuration + # by using the `sim_settings`. `sim_settings` are meant to configure the + # creation of the simulation model and assign information before starting + # the process. This can be either weather data files, default simulation + # time of the created model but also what kind of enrichment should be used + # or what elements are relevant for the simulation. For more information + # please review the documentation for `sim_settings`. + + # Let's assign a weather file first. This is currently needed, even if no + # simulation is performed + project.sim_settings.weather_file_path = ( + Path(bim2sim.__file__).parent.parent / + 'test/resources/weather_files/DEU_NW_Aachen.105010_TMYx.mos') + + # assign an IDS file, which is needed to check the ifc file by ifctester + project.sim_settings.ids_file_path = ( + Path(bim2sim.__file__).parent.parent / + 'test/resources/ids/' + 'fail-a_minimal_ids_can_check_a_minimal_ifc_1_2.ids' + ) + + # run the project + run_project(project, ConsoleDecisionHandler()) + + +if __name__ == '__main__': + run_simple_project() diff --git a/bim2sim/plugins/PluginIFCCheck/bim2sim_ifccheck/examples/e1_checkIFCIDS_bps.py b/bim2sim/plugins/PluginIFCCheck/bim2sim_ifccheck/examples/e1_checkIFCIDS_bps.py new file mode 100644 index 0000000000..dd24f40ba5 --- /dev/null +++ b/bim2sim/plugins/PluginIFCCheck/bim2sim_ifccheck/examples/e1_checkIFCIDS_bps.py @@ -0,0 +1,56 @@ +"""Simple example for PluginIFCCheck with bps focus.""" +import tempfile +from pathlib import Path + +import bim2sim +from bim2sim import Project, ConsoleDecisionHandler, run_project +from bim2sim.utilities.types import IFCDomain +from bim2sim.plugins.PluginIFCCheck.bim2sim_ifccheck import PluginIFCCheck + + +def run_simple_project(): + """Run a bim2sim project with the PluginIFCCheck.""" + # Create a temp directory for the project, feel free to use a "normal" + # directory + project_path = Path(tempfile.TemporaryDirectory( + prefix='bim2sim_e1_checkifc_').name) + + # Set the ifc path to use and define which domain the IFC belongs to. + # This is done via a dictionary, where the key is the domain and the value + # the path to the IFC file. We are using an architecture domain IFC file + # here from the FZK-Haus which is a simple IFC provided by KIT. + ifc_paths = { + IFCDomain.arch: + Path(bim2sim.__file__).parent.parent / + 'test/resources/arch/ifc/AC20-FZK-Haus.ifc', + } + + project = Project.create( + project_path, ifc_paths, PluginIFCCheck) + + # Next to the plugin that should be used we can do further configuration + # by using the `sim_settings`. `sim_settings` are meant to configure the + # creation of the simulation model and assign information before starting + # the process. This can be either weather data files, default simulation + # time of the created model but also what kind of enrichment should be used + # or what elements are relevant for the simulation. For more information + # please review the documentation for `sim_settings`. + + # Let's assign a weather file first. This is currently needed, even if no + # simulation is performed + project.sim_settings.weather_file_path = ( + Path(bim2sim.__file__).parent.parent / + 'test/resources/weather_files/DEU_NW_Aachen.105010_TMYx.mos') + + # assign an IDS file, which is needed to check the ifc file by ifctester + project.sim_settings.ids_file_path = ( + Path(bim2sim.__file__).parent / + 'plugins/PluginIFCCheck/bim2sim_ifccheck/ifc_bps.ids' + ) + + # run the project + run_project(project, ConsoleDecisionHandler()) + + +if __name__ == '__main__': + run_simple_project() diff --git a/bim2sim/plugins/PluginIFCCheck/bim2sim_ifccheck/examples/e1_checkIFCIDS_bps_3rooms.py b/bim2sim/plugins/PluginIFCCheck/bim2sim_ifccheck/examples/e1_checkIFCIDS_bps_3rooms.py new file mode 100644 index 0000000000..44edcd3f3f --- /dev/null +++ b/bim2sim/plugins/PluginIFCCheck/bim2sim_ifccheck/examples/e1_checkIFCIDS_bps_3rooms.py @@ -0,0 +1,74 @@ +"""Simple example for PluginIFCCheck with bps focus.""" +import tempfile +from pathlib import Path + +import bim2sim +from bim2sim import Project +from bim2sim.kernel.decision.decisionhandler import DebugDecisionHandler +from bim2sim.utilities.types import IFCDomain +from bim2sim.plugins.PluginIFCCheck.bim2sim_ifccheck import PluginIFCCheck + + +def run_simple_project(): + """Run a bim2sim project with the PluginIFCCheck.""" + # Create a temp directory for the project, feel free to use a "normal" + # directory + project_path = Path(tempfile.TemporaryDirectory( + prefix='bim2sim_e1_checkifc_bps_3rooms_').name) + + # Set the ifc path to use and define which domain the IFC belongs to. + # This is done via a dictionary, where the key is the domain and the value + # the path to the IFC file. We are using an architecture domain IFC file + # here from the FZK-Haus which is a simple IFC provided by KIT. + ifc_paths = { + IFCDomain.arch: + Path(bim2sim.__file__).parent.parent / + 'test/resources/arch/ifc/' + '3rooms_Heater_AirTerminal_Table_with_SB_errors.ifc', + } + project = Project.create( + project_path, ifc_paths, PluginIFCCheck) + + # Next to the plugin that should be used we can do further configuration + # by using the `sim_settings`. `sim_settings` are meant to configure the + # creation of the simulation model and assign information before starting + # the process. This can be either weather data files, default simulation + # time of the created model but also what kind of enrichment should be used + # or what elements are relevant for the simulation. For more information + # please review the documentation for `sim_settings`. + + # Let's assign a weather file first. This is currently needed, even if no + # simulation is performed + project.sim_settings.weather_file_path = ( + Path(bim2sim.__file__).parent.parent / + 'test/resources/weather_files/DEU_NW_Aachen.105010_TMYx.mos') + + # assign an IDS file, which is needed to check the ifc file by ifctester + project.sim_settings.ids_file_path = ( + Path(bim2sim.__file__).parent / + 'plugins/PluginIFCCheck/bim2sim_ifccheck/ifc_bps.ids' + ) + + # Before we can run the project, we need to assign a DecisionHandler. To + # understand this, we need to understand why we need such a handler. + # Decisions in bim2sim are used to get user input whenever information in + # the IFC are unclear. E.g. if the usage type of a room can't be + # identified, we use a decision to query the user what usage the room has. + # As we don't know at which point a decision comes up, we are using + # generators and yield to iterate over them. If you want to understand + # deeper how this works, have a look at the decision documentation. + # For usage as console tool, we implemented the ConsoleDecisionHandler, + # which we are going to assign in the next step. + # There are multiple ways to run a project. One is to use the run_project() + # function and assign which project to run and which decision handler to + # use. In our case this is: + + # Run the project with pre-configured answers for decisions + answers = ('Other', + ) + Handler = DebugDecisionHandler(answers) + Handler.handle(project.run()) + + +if __name__ == '__main__': + run_simple_project() diff --git a/bim2sim/plugins/PluginIFCCheck/bim2sim_ifccheck/examples/e1_checkIFCIDS_hvac.py b/bim2sim/plugins/PluginIFCCheck/bim2sim_ifccheck/examples/e1_checkIFCIDS_hvac.py new file mode 100644 index 0000000000..d37b55a1a5 --- /dev/null +++ b/bim2sim/plugins/PluginIFCCheck/bim2sim_ifccheck/examples/e1_checkIFCIDS_hvac.py @@ -0,0 +1,55 @@ +"""Simple example for PluginIFCCheck with hvac focus.""" +import tempfile +from pathlib import Path + +import bim2sim +from bim2sim import Project, ConsoleDecisionHandler, run_project +from bim2sim.utilities.types import IFCDomain +from bim2sim.plugins.PluginIFCCheck.bim2sim_ifccheck import PluginIFCCheck + + +def run_simple_project(): + """Run a bim2sim project with the PluginIFCCheck.""" + # Create a temp directory for the project, feel free to use a "normal" + # directory + project_path = Path(tempfile.TemporaryDirectory( + prefix='bim2sim_e1_checkifc_hvac_').name) + + # Set the ifc path to use and define which domain the IFC belongs to. + # This is done via a dictionary, where the key is the domain and the value + # the path to the IFC file. We are using an architecture domain IFC file + # here from the FZK-Haus which is a simple IFC provided by KIT. + ifc_paths = { + IFCDomain.hydraulic: + Path(bim2sim.__file__).parent.parent / + 'test/resources/hydraulic/ifc/hvac_heating.ifc', + } + + project = Project.create( + project_path, ifc_paths, PluginIFCCheck) + + # Next to the plugin that should be used we can do further configuration + # by using the `sim_settings`. `sim_settings` are meant to configure the + # creation of the simulation model and assign information before starting + # the process. This can be either weather data files, default simulation + # time of the created model but also what kind of enrichment should be used + # or what elements are relevant for the simulation. For more information + # please review the documentation for `sim_settings`. + + # Let's assign a weather file first. This is currently needed, even if no + # simulation is performed + project.sim_settings.weather_file_path = ( + Path(bim2sim.__file__).parent.parent / + 'test/resources/weather_files/DEU_NW_Aachen.105010_TMYx.mos') + + project.sim_settings.ids_file_path = ( + Path(bim2sim.__file__).parent / + 'plugins/PluginIFCCheck/bim2sim_ifccheck/ifc_hvac.ids' + ) + + # run the project + run_project(project, ConsoleDecisionHandler()) + + +if __name__ == '__main__': + run_simple_project() diff --git a/bim2sim/plugins/PluginIFCCheck/bim2sim_ifccheck/ifc_bps.ids b/bim2sim/plugins/PluginIFCCheck/bim2sim_ifccheck/ifc_bps.ids new file mode 100644 index 0000000000..f9f6ce2d2c --- /dev/null +++ b/bim2sim/plugins/PluginIFCCheck/bim2sim_ifccheck/ifc_bps.ids @@ -0,0 +1,273 @@ + + + + Ifc_properties + 0.0.1 + fcudok@rom-technik.de + 2025-08-26 + + + + + + + GlobalId + + + + + + + GlobalId + + + + + + + + + + + + + + IFCWINDOW + + + + + + + Pset_WindowCommon + + + ThermalTransmittance + + + + + + + + + + + + + + + + + + + Pset_SlabCommon + + + ThermalTransmittance + + + + + + + + + + + + + + + + + + + Pset_WallCommon + + + ThermalTransmittance + + + + + + + + + IFCBUILDING + + + + + + + Pset_BuildingCommon + + + YearOfConstruction + + + + + Pset_BuildingCommon + + + OccupancyType + + + + + + + + + IFCBUILDINGSTOREY + + + + + + + + + + + + GrossFloorArea + + + + + + + + + + GrossHeight + + + + + + + + + + Height + + + + + + + + + + NetHeight + + + + + + + + + IFCSPACE + + + + + + + IFCSPACE + + + + + + + + + IFCRELSPACEBOUNDARY2NDLEVEL + + + + + + + RelatingSpace + + + + + + + RelatedBuildingElement + + + + + + + Description + + + + + + + + + + ConnectionGeometry + + + + + PhysicalOrVirtualBoundary + + + + + + + + + + InternalOrExternalBoundary + + + + + + + + + + + + + + IFCRELSPACEBOUNDARY2NDLEVEL + + + + + CorrespondingBoundary + + + + + + + Description + + + 2a + + + + + + diff --git a/bim2sim/plugins/PluginIFCCheck/bim2sim_ifccheck/ifc_hvac.ids b/bim2sim/plugins/PluginIFCCheck/bim2sim_ifccheck/ifc_hvac.ids new file mode 100644 index 0000000000..86905a7bee --- /dev/null +++ b/bim2sim/plugins/PluginIFCCheck/bim2sim_ifccheck/ifc_hvac.ids @@ -0,0 +1,53 @@ + + + + Ifc_properties + 0.0.1 + fcudok@rom-technik.de + 2025-08-26 + + + + + + + GlobalId + + + + + + + GlobalId + + + + + + + + + + + + + + IFCDISTRIBUTIONPORT + + + + + + + FlowDirection + + + + + + + + + + + diff --git a/bim2sim/plugins/PluginIFCCheck/bim2sim_ifccheck/sim_settings.py b/bim2sim/plugins/PluginIFCCheck/bim2sim_ifccheck/sim_settings.py new file mode 100644 index 0000000000..ad2614c314 --- /dev/null +++ b/bim2sim/plugins/PluginIFCCheck/bim2sim_ifccheck/sim_settings.py @@ -0,0 +1,23 @@ +from bim2sim.sim_settings import BaseSimSettings, PathSetting +# from bim2sim.utilities.types import LOD, ZoningCriteria + + +class CheckIFCSimSettings(BaseSimSettings): + """Defines simulation settings for Check IFC Plugin. + + This class defines the "simulation" settings for the Check IFC Plugin. It + inherits all choices from the BaseSimSettings settings. Specific settings + for the IFC Check are added here. + + """ + + ids_file_path = PathSetting( + value=None, + description='Path to the IDS(Information Delivery Specification) file' + 'that should be used for the check of the IFC file. ' + 'The file is a xml file, with .ids extension' + 'An Example can be find in' + '/bim2sim/plugins/PluginIFCCheck/bim2sim_ifccheck', + for_frontend=True, + mandatory=True + ) diff --git a/bim2sim/plugins/PluginIFCCheck/bim2sim_ifccheck/task/__init__.py b/bim2sim/plugins/PluginIFCCheck/bim2sim_ifccheck/task/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/bim2sim/plugins/PluginIFCCheck/bim2sim_ifccheck/task/template_task.py b/bim2sim/plugins/PluginIFCCheck/bim2sim_ifccheck/task/template_task.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/bim2sim/plugins/PluginIFCCheck/test/__init__.py b/bim2sim/plugins/PluginIFCCheck/test/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/bim2sim/plugins/PluginIFCCheck/test/integration/test_ifccheck.py b/bim2sim/plugins/PluginIFCCheck/test/integration/test_ifccheck.py new file mode 100644 index 0000000000..2eab66016d --- /dev/null +++ b/bim2sim/plugins/PluginIFCCheck/test/integration/test_ifccheck.py @@ -0,0 +1,104 @@ +import unittest +from pathlib import Path + +import bim2sim +from bim2sim.kernel.decision.decisionhandler import DebugDecisionHandler +from bim2sim.utilities.test import IntegrationBase +from bim2sim.utilities.types import IFCDomain +from bim2sim.plugins.PluginIFCCheck.bim2sim_ifccheck import PluginIFCCheck + + +class IntegrationBaseIFCCheck(IntegrationBase): + def model_domain_path(self) -> str: + return 'arch' + + +class TestIntegrationIFCCheck(IntegrationBaseIFCCheck, unittest.TestCase): + def test_run_fzk_hause(self): + """Run project with AC20-FZK-Haus.ifc""" + ifc_names = {IFCDomain.arch: 'AC20-FZK-Haus.ifc'} + project = self.create_project(ifc_names, PluginIFCCheck) + + # assign an IDS file, + # which is needed to check the ifc file by ifctester + project.sim_settings.ids_file_path = ( + Path(bim2sim.__file__).parent / + 'plugins/PluginIFCCheck/bim2sim_ifccheck/ifc_bps.ids' + ) + + answers = () + handler = DebugDecisionHandler(answers) + for decision, answer in handler.decision_answer_mapping(project.run()): + decision.value = answer + self.assertEqual(0, handler.return_value, + "Project did not finish successfully.") + + def test_run_fzk_hause_sb55(self): + """Run project with AC20-FZK-Haus.ifc""" + ifc_names = {IFCDomain.arch: 'AC20-FZK-Haus_with_SB55.ifc'} + project = self.create_project(ifc_names, PluginIFCCheck) + + # assign an IDS file, + # which is needed to check the ifc file by ifctester + project.sim_settings.ids_file_path = ( + Path(bim2sim.__file__).parent / + 'plugins/PluginIFCCheck/bim2sim_ifccheck/ifc_bps.ids' + ) + + answers = ('Other', 'Other') + handler = DebugDecisionHandler(answers) + for decision, answer in handler.decision_answer_mapping(project.run()): + decision.value = answer + self.assertEqual(0, handler.return_value, + "Project did not finish successfully.") + + def test_run_three_rooms_with_SB(self): + """Run project with + 2024-04-23_3rooms_240317_Heater_AirTerminal_Table_with_SB.ifc""" + + ifc_3rooms_with_sb = (Path( + bim2sim.__file__).parent.parent / "test/resources/arch/ifc/" + "3rooms_Heater_AirTerminal_Table_with_SB.ifc") + ifc_names = {IFCDomain.arch: ifc_3rooms_with_sb} + project = self.create_project(ifc_names, PluginIFCCheck) + + # assign an IDS file, + # which is needed to check the ifc file by ifctester + project.sim_settings.ids_file_path = ( + Path(bim2sim.__file__).parent / + 'plugins/PluginIFCCheck/bim2sim_ifccheck/ifc_bps.ids' + ) + + answers = ('Other', 'Other') + handler = DebugDecisionHandler(answers) + for decision, answer in handler.decision_answer_mapping(project.run()): + decision.value = answer + self.assertEqual(0, handler.return_value, + "Project did not finish successfully.") + + def test_run_three_rooms_with_SB_fail(self): + """Run project with + 2024-04-23_3rooms_240317_Heater_AirTerminal_Table_with_SB_errors.ifc""" + + ifc_3rooms_with_sb_fail = (Path( + bim2sim.__file__).parent.parent / "test/resources/arch/ifc/" + "3rooms_Heater_AirTerminal_Table_with_SB_errors.ifc") + + ifc_names = {IFCDomain.arch: ifc_3rooms_with_sb_fail} + project = self.create_project(ifc_names, PluginIFCCheck) + + # assign an IDS file, + # which is needed to check the ifc file by ifctester + project.sim_settings.ids_file_path = ( + Path(bim2sim.__file__).parent / + 'plugins/PluginIFCCheck/bim2sim_ifccheck/ifc_bps.ids' + ) + + project.sim_settings.setpoints_from_template = True + + answers = ('Other', 'Other') + handler = DebugDecisionHandler(answers) + for decision, answer in handler.decision_answer_mapping(project.run()): + decision.value = answer + self.assertEqual(0, handler.return_value, + "Project did not finish successfully.") diff --git a/bim2sim/plugins/PluginIFCCheck/test/integration/test_usage.py b/bim2sim/plugins/PluginIFCCheck/test/integration/test_usage.py new file mode 100644 index 0000000000..90f0eac873 --- /dev/null +++ b/bim2sim/plugins/PluginIFCCheck/test/integration/test_usage.py @@ -0,0 +1,18 @@ +import unittest + + +class TestUsage(unittest.TestCase): + """Tests for general use of library""" + + def test_import_plugin(self): + """Test importing IFCCheck plugin in python script""" + try: + from bim2sim.plugins import load_plugin, Plugin + plugin = load_plugin('bim2sim_ifccheck') + assert issubclass(plugin, Plugin) + except ImportError as err: + self.fail("Unable to import plugin\nreason: %s"%(err)) + + +if __name__ == '__main__': + unittest.main() diff --git a/bim2sim/plugins/PluginIFCCheck/test/regression/test_ifccheck.py b/bim2sim/plugins/PluginIFCCheck/test/regression/test_ifccheck.py new file mode 100644 index 0000000000..30059a0a76 --- /dev/null +++ b/bim2sim/plugins/PluginIFCCheck/test/regression/test_ifccheck.py @@ -0,0 +1,449 @@ +"""regession tests for the pluging IFC Check""" +import logging +import unittest +from lxml import html +from pathlib import Path + + +import bim2sim +from bim2sim.kernel.decision.decisionhandler import DebugDecisionHandler +from bim2sim.utilities.test import RegressionTestBase +from bim2sim.utilities.types import IFCDomain +from bim2sim.plugins.PluginIFCCheck.bim2sim_ifccheck import PluginIFCCheck + +logger = logging.getLogger(__name__) + + +class Gen_xpath: + """for genaration of xpaths.""" + + def __init__(self, xpath_incl_var): + """Initiate Gen_xpath class. + + Args: + xpaht_incl_var: xpaht including placeholder (here '{}') + """ + self.xpath_incl_var = xpath_incl_var + + def gen_xpath_name_pos(self, name_ele: str, pos_ele: int) -> str: + """Generate a xpart in html report for checks. + + Args: + name_ele: name of the element + pos_ele: number of the item in the element + Retruns: + xpath: str + example = ('//p/span[@class="item" and ' + + 'contains(normalize-space(.),"{}")]/ strong[{}]') + """ + xpath = self.xpath_incl_var.format(name_ele, pos_ele) + return xpath + + def gen_xpath_name(self, name_ele: str) -> str: + """Generate a xpart in html report for checks. + + Args: + name_ele: name of the element + Retruns: + xpath: str + example = ('//p/span[@class="item" and ' + + 'contains(normalize-space(.),"{}")]/') + """ + xpath = self.xpath_incl_var.format(name_ele) + return xpath + + +class RegressionTestIFCCheck(RegressionTestBase): + """Class to setup up nad run regression test of PluginIFCCheck""" + def setUp(self): + self.results_src_dir = None + self.results_dst_dir = None + super().setUp() + + def tearDown(self): + super().tearDown() + + @staticmethod + def get_elements_html_file(filepath, xpaths): + """Get specific elements for a html file. + + Args: + filepath: path to a html file + xpaths: list of xpaths (strings), + like '//div[@class="fail percent"]' + Returns: + results: list of the specific elements + """ + with open(filepath, 'r') as f: + html_code = f.read() + doc = html.fromstring(html_code) + results = [] + for xpath in xpaths: + elem = doc.xpath(xpath) + elem_str = elem[0].text_content().strip() + results.append(elem_str) + return results + + def run_regression_test(self): + """Run the regression test.""" + + # html elements from test resources + result_res = self.get_elements_html_file( + self.path_result_file_ifc_tester_res, + self.xpaths) + # html elements from generated reports (while test run) + result_test = self.get_elements_html_file( + self.path_result_file_ifc_tester, + self.xpaths) + reg_test_res = (result_res == result_test) + + return [reg_test_res, result_res, result_test] + + +class TestRegressionIFCCheck(RegressionTestIFCCheck, unittest.TestCase): + def test_run_ifc_check_fzk_haus_ids(self): + """Run IFCCheck regression test with AC20-FZK-Haus.ifc + checking the html report generated by IfcTester + """ + + ifc_names = {IFCDomain.arch: 'AC20-FZK-Haus.ifc'} + project = self.create_project(ifc_names, PluginIFCCheck) + + # assign an IDS file, + # which is needed to check the ifc file by ifctester + project.sim_settings.ids_file_path = ( + Path(bim2sim.__file__).parent / + 'plugins/PluginIFCCheck/bim2sim_ifccheck/ifc_bps.ids' + ) + + answers = () + handler = DebugDecisionHandler(answers) + + handler.handle(project.run()) + + # paths result file ifc tester (ids) + self.path_result_file_ifc_tester = ( + self.project.paths.log / "ifc_ids_check.html" + ) + self.path_result_file_ifc_tester_res = ( + Path(bim2sim.__file__).parent.parent / + 'test/resources/ifc_check/regression_results/ifc_ids_check.html' + ) + + # xpaths to elements in html + # fail percent (unique) + xpath_fail_perc = '//div[@class="fail percent"]' + # create function for xpath generation + xpath_ids_var = Gen_xpath( + xpath_incl_var = ( + '//p/span[@class="item" and ' + + 'contains(normalize-space(.),"{}")]/ strong[{}]' + )) + + # Specifications passed + xpath_spec_pass = xpath_ids_var.gen_xpath_name_pos( + "Specifications passed", 1) + # Specifications total + xpath_spec_total = xpath_ids_var.gen_xpath_name_pos( + "Specifications passed", 2) + # Requirements passed + xpath_req_pass = xpath_ids_var.gen_xpath_name_pos( + "Requirements passed", 1) + # Requirements total + xpath_req_total = xpath_ids_var.gen_xpath_name_pos( + "Requirements passed", 2) + # Checks passed + xpath_checks_pass = xpath_ids_var.gen_xpath_name_pos( + "Checks passed", 1) + # Checks total + xpath_checks_total = xpath_ids_var.gen_xpath_name_pos( + "Checks passed", 2) + + self.xpaths = [ + xpath_fail_perc, + xpath_spec_pass, xpath_spec_total, + xpath_req_pass, xpath_req_total, + xpath_checks_pass, xpath_checks_total] + + reg_result = self.run_regression_test() + + self.assertTrue(reg_result[0], "Comparison of the ifc tester " + + "html reports (IDS) fails. " + + "expected: {} ".format(reg_result[1]) + + "test: {}".format(reg_result[2])) + + def test_run_ifc_check_fzk_haus_sum_prop(self): + """Run IFCCheck regression test with AC20-FZK-Haus.ifc checking + the html report "error_summary_prop" generated by bim2sim specific + checks. + + The report, which is partly named "error_summary_prop" + includes: summary of missing properties + """ + + ifc_names = {IFCDomain.arch: 'AC20-FZK-Haus.ifc'} + project = self.create_project(ifc_names, PluginIFCCheck) + + # assign an IDS file, which is needed to check the ifc file + # by ifctester + project.sim_settings.ids_file_path = ( + Path(bim2sim.__file__).parent / + 'plugins/PluginIFCCheck/bim2sim_ifccheck/ifc_bps.ids' + ) + + answers = () + handler = DebugDecisionHandler(answers) + + handler.handle(project.run()) + + # paths result file ifc tester (ids) + self.path_result_file_ifc_tester = ( + self.project.paths.log / + "ARCH_AC20-FZK-Haus_error_summary_prop.html" + ) + self.path_result_file_ifc_tester_res = ( + Path(bim2sim.__file__).parent.parent / + 'test/resources/ifc_check/regression_results/' / + 'ARCH_AC20-FZK-Haus_error_summary_prop.html' + ) + + # xpaths to elements in html + # create function for xpath generation + xpath_prop_var = Gen_xpath( + xpath_incl_var=( + '//tr[td[@class="tg-fymr" and ' + 'normalize-space(.)="{}"]]' + '/td[@class="tg-dvpl"][normalize-space()]' + )) + + # Total IFCProduct with missing properties + xpath_prod_with_miss_prop_total = xpath_prop_var.gen_xpath_name( + "Total IFCProduct with missing properties:") + # Total missing properties in IFCProducts: + xpath_miss_prop_in_prod_total = xpath_prop_var.gen_xpath_name( + "Total missing properties in IFCProducts:") + + self.xpaths = [ + xpath_prod_with_miss_prop_total, + xpath_miss_prop_in_prod_total, + ] + + reg_result = self.run_regression_test() + + self.assertTrue(reg_result[0], "Comparison of the 'summary prop' " + + "html reports (bim2sim specific check) fails. " + + "expected: {} ".format(reg_result[1]) + + "test: {}".format(reg_result[2])) + + def test_run_ifc_check_fzk_haus_sum_inst(self): + """Run IFCCheck regression test with AC20-FZK-Haus.ifc + checking the html report "error_summary_inst" generated + by bim2sim specific checks. + + The report, which is partly named "error_summary_inst" + includes: summary of errors related to elements + """ + + ifc_names = {IFCDomain.arch: 'AC20-FZK-Haus.ifc'} + project = self.create_project(ifc_names, PluginIFCCheck) + + # assign an IDS file, which is needed to check the ifc file + # by ifctester + project.sim_settings.ids_file_path = ( + Path(bim2sim.__file__).parent / + 'plugins/PluginIFCCheck/bim2sim_ifccheck/ifc_bps.ids' + ) + + answers = () + handler = DebugDecisionHandler(answers) + + handler.handle(project.run()) + + # paths result file ifc tester (ids) + self.path_result_file_ifc_tester = ( + self.project.paths.log / + "ARCH_AC20-FZK-Haus_error_summary_inst.html" + ) + self.path_result_file_ifc_tester_res = ( + Path(bim2sim.__file__).parent.parent / + 'test/resources/ifc_check/regression_results/' / + 'ARCH_AC20-FZK-Haus_error_summary_inst.html' + ) + + # xpaths to elements in html + # create function for xpath generation + xpath_inst_var = Gen_xpath( + xpath_incl_var=( + '//tr[td[@class="tg-fymr" and ' + + 'normalize-space(.)="{}"]]' + + '/td[@class="tg-dvpl"][normalize-space()]')) + # Total IFCProduct with errors + xpath_prod_with_errors_total = xpath_inst_var.gen_xpath_name( + "Total IFCProduct with errors:") + # Total errors in IFCProducts: + xpath_errors_in_prod_total = xpath_inst_var.gen_xpath_name( + "Total errors in IFCProducts:") + # Total IfcRelSpaceBoundary with errors: + xpath_errors_in_sb_total = xpath_inst_var.gen_xpath_name( + "Total IfcRelSpaceBoundary with errors:") + # Total errors in IfcRelSpaceBoundary: + xpath_errors_in_relsb_total = xpath_inst_var.gen_xpath_name( + "Total errors in IfcRelSpaceBoundary:") + + self.xpaths = [ + xpath_prod_with_errors_total, + xpath_errors_in_prod_total, + xpath_errors_in_sb_total, + xpath_errors_in_relsb_total, + ] + + reg_result = self.run_regression_test() + + self.assertTrue(reg_result[0], "Comparison of the 'summary inst' " + + "html reports (bim2sim specific check) fails. " + + "expected: {} ".format(reg_result[1]) + + "test: {}".format(reg_result[2])) + + def test_run_ifc_check_fzk_haus_sum(self): + """Run IFCCheck regression test with AC20-FZK-Haus.ifc + checking the html report "error_summary" generated + by bim2sim specific checks. + + The report, which is partly named "error_summary" + includes: overview of all errors + """ + + ifc_names = {IFCDomain.arch: 'AC20-FZK-Haus.ifc'} + project = self.create_project(ifc_names, PluginIFCCheck) + + # assign an IDS file, which is needed to check the ifc file + # by ifctester + project.sim_settings.ids_file_path = ( + Path(bim2sim.__file__).parent / + 'plugins/PluginIFCCheck/bim2sim_ifccheck/ifc_bps.ids' + ) + + answers = () + handler = DebugDecisionHandler(answers) + + handler.handle(project.run()) + + # paths result file ifc tester (ids) + self.path_result_file_ifc_tester = ( + self.project.paths.log / + "ARCH_AC20-FZK-Haus_error_summary.html" + ) + self.path_result_file_ifc_tester_res = ( + Path(bim2sim.__file__).parent.parent / + 'test/resources/ifc_check/regression_results/' / + 'ARCH_AC20-FZK-Haus_error_summary.html' + ) + + # xpaths to elements in html + # create function for xpath generation + xpath_inst_var = Gen_xpath( + xpath_incl_var=( + '//tr[td[@class="tg-dvpl" and ' + + 'normalize-space(.)="{}"]]' + + '/td[@class="tg-dvpl"][2]')) + + # IFC Version: + xpath_ifc_version = xpath_inst_var.gen_xpath_name( + "IFC Version:") + # Version error: + xpath_ifc_version_error = xpath_inst_var.gen_xpath_name( + "Version error:") + # Unique GUID: + xpath_unique_guid = xpath_inst_var.gen_xpath_name( + "Unique GUID:") + # Total non-unique GUIDs: + xpath_nonunique_guid_total = xpath_inst_var.gen_xpath_name( + "Total non-unique GUIDs:") + # Empty GUID: + xpath_empty_guid = xpath_inst_var.gen_xpath_name( + "Empty GUID:") + # Total Empty GUIDs: + xpath_empty_guid_total = xpath_inst_var.gen_xpath_name( + "Total Empty GUIDs:") + # Total IFCProduct with errors + xpath_prod_with_errors_total = xpath_inst_var.gen_xpath_name( + "Total IFCProduct with errors:") + + self.xpaths = [ + xpath_ifc_version, + xpath_ifc_version_error, + xpath_unique_guid, + xpath_nonunique_guid_total, + xpath_empty_guid, + xpath_empty_guid_total, + xpath_prod_with_errors_total, + ] + + reg_result = self.run_regression_test() + + self.assertTrue(reg_result[0], "Comparison of the 'summary' " + + "html reports (bim2sim specific check) fails. " + + "expected: {} ".format(reg_result[1]) + + "test: {}".format(reg_result[2])) + + def test_run_ifc_check_fzk_haus_guid(self): + """Run IFCCheck regression test with AC20-FZK-Haus.ifc + checking the html report "error_summary_guid" generated + by bim2sim specific checks. + + The report, which is partly named "error_summary_guid" + includes: summary of GUID errors + """ + + ifc_names = {IFCDomain.arch: 'AC20-FZK-Haus.ifc'} + project = self.create_project(ifc_names, PluginIFCCheck) + + # assign an IDS file, which is needed to check the ifc file + # by ifctester + project.sim_settings.ids_file_path = ( + Path(bim2sim.__file__).parent / + 'plugins/PluginIFCCheck/bim2sim_ifccheck/ifc_bps.ids' + ) + + answers = () + handler = DebugDecisionHandler(answers) + + handler.handle(project.run()) + + # paths result file ifc tester (ids) + self.path_result_file_ifc_tester = ( + self.project.paths.log / + "ARCH_AC20-FZK-Haus_error_summary_guid.html" + ) + self.path_result_file_ifc_tester_res = ( + Path(bim2sim.__file__).parent.parent / + 'test/resources/ifc_check/regression_results/' / + 'ARCH_AC20-FZK-Haus_error_summary_guid.html' + ) + + # xpaths to elements in html + # create function for xpath generation + xpath_inst_var = Gen_xpath( + xpath_incl_var=( + '//tr[td[@class="tg-fymr" and ' + + 'normalize-space(.)="{}"]]' + + '/td[@class="tg-dvpl"][1]')) + + # Total non-unique GUIDs: + xpath_nonunique_guid_total = xpath_inst_var.gen_xpath_name( + "Total non-unique GUIDs:") + # Total Empty GUIDs: + xpath_empty_guid_total = xpath_inst_var.gen_xpath_name( + "Total Empty GUIDs:") + + self.xpaths = [ + xpath_nonunique_guid_total, + xpath_empty_guid_total, + ] + + reg_result = self.run_regression_test() + + self.assertTrue(reg_result[0], "Comparison of the 'summary_guid' " + + "html reports (bim2sim specific check) fails. " + + "expected: {} ".format(reg_result[1]) + + "test: {}".format(reg_result[2])) diff --git a/bim2sim/plugins/PluginIFCCheck/test/unit/test_check_ifc.py b/bim2sim/plugins/PluginIFCCheck/test/unit/test_check_ifc.py new file mode 100644 index 0000000000..1c46ff9ba5 --- /dev/null +++ b/bim2sim/plugins/PluginIFCCheck/test/unit/test_check_ifc.py @@ -0,0 +1,441 @@ +"""Tests for the ifc check of bim2sim + +Based on ifctester and IDS (Information Delivery Specification), but also +checks not based on ifctester (some requirements are not able to check +with IDS v10) + +""" + +import unittest +import tempfile +from pathlib import Path + +import bim2sim.tasks.common.load_ifc +from bim2sim.kernel.decision.decisionhandler import DebugDecisionHandler +from bim2sim.plugins import Plugin +from bim2sim.project import Project +from bim2sim.plugins.PluginIFCCheck.bim2sim_ifccheck.sim_settings import \ + CheckIFCSimSettings +from bim2sim.utilities.types import IFCDomain + +from bim2sim.tasks.checks.check_ifc import CheckIfc, CheckLogicBase + +test_rsrc_path = ( + Path(__file__).parent.parent.parent.parent.parent. + parent / 'test/resources' + ) +default_ids_file_path = ( + Path(__file__).parent.parent.parent / 'bim2sim_ifccheck' + ) + +class PluginDummy(Plugin): + name = 'test' + sim_settings = CheckIFCSimSettings + default_tasks = [ + bim2sim.tasks.common.load_ifc.LoadIFC, + ] + + +class TestCheckIFC(unittest.TestCase): + """Tests for function checking IFC files, which are self made (these needed + features are not included in the library ifctester respectifly in IDS + standard + + """ + + def tearDown(self): + self.project.finalize(True) + self.test_dir.cleanup() + + def weather_file_path(self) -> Path: + return (test_rsrc_path / + 'weather_files/DEU_NW_Aachen.105010_TMYx.epw') + + def ids_file_path(self) -> Path: + return (default_ids_file_path / + 'ifc_bps.ids') + + ifc_file_fkz = (Path(bim2sim.__file__).parent.parent / + 'test/resources/arch/ifc/AC20-FZK-Haus.ifc') + ifc_file_fkz_DoubleAndNoneGUID = ( + Path(bim2sim.__file__).parent.parent / + 'test/resources/arch/ifc/AC20-FZK-Haus_NoneAndDoubleGUID.ifc') + ifc_file_fkz_SB55_DoubleAndNoneGUID = ( + Path(bim2sim.__file__).parent.parent / + 'test/resources/arch/ifc/AC20-FZK-Haus_with_SB55_NoneAndDoubleGUID.ifc' + ) + ifc_file_fkz_SB55 = ( + Path(bim2sim.__file__).parent.parent / + 'test/resources/arch/ifc/AC20-FZK-Haus_with_SB55.ifc') + ifc_file_fkz_ifc23 = ( + Path(bim2sim.__file__).parent.parent / + 'test/resources/arch/ifc/AC20-FZK-Haus_ifc23.ifc') + + def test_check_guid_unique_pass(self): + """test the boolean of the GUID uniqueness check, check pass + the following represent a project using the DummyPlugin + """ + + self.test_dir = tempfile.TemporaryDirectory() + ifc_paths = { + IFCDomain.arch: self.ifc_file_fkz_SB55, + } + self.project = Project.create(self.test_dir.name, ifc_paths, + plugin=PluginDummy, + ) + # weather data path is mandatory and "mocking" is not working + # so use a central defintion of weather file + self.project.sim_settings.weather_file_path = self.weather_file_path() + self.project.sim_settings.ids_file_path = self.ids_file_path() + # put project.run into DebugDecisionHandler is need, otherwise the + # playground.state() is empty and ifc_files are not available + # default answer for decision questions + answers = ('Other',) + handler = DebugDecisionHandler(answers) + handler.handle(self.project.run(cleanup=False)) + + ifc_files = self.project.playground.state['ifc_files'] + + for ifc_file in ifc_files: + # self.run_check_guid_unique(ifc_file) + all_guids_checks_passed, non_unique_guids = ( + CheckLogicBase.run_check_guid_unique(ifc_file) + ) + self.assertEqual(all_guids_checks_passed, True, "Should be True") + + def test_check_guid_unique_fail(self): + """test the boolean of the GUID uniqueness check, check fail + the following represent a project using the DummyPlugin + """ + + self.test_dir = tempfile.TemporaryDirectory() + ifc_paths = { + IFCDomain.arch: self.ifc_file_fkz_SB55_DoubleAndNoneGUID, + } + self.project = Project.create(self.test_dir.name, ifc_paths, + plugin=PluginDummy, ) + # weather data path is mandatory and "mocking" is not working + # so use a central defintion of weather file + self.project.sim_settings.weather_file_path = self.weather_file_path() + self.project.sim_settings.ids_file_path = self.ids_file_path() + # put project.run into DebugDecisionHandler is need, otherwise the + # playground.state() is empty and ifc_files are not available + # default answer for decision questions + answers = ('Other', 'Other',) + handler = DebugDecisionHandler(answers) + handler.handle(self.project.run(cleanup=False)) + + ifc_files = self.project.playground.state['ifc_files'] + + for ifc_file in ifc_files: + # self.run_check_guid_unique(ifc_file) + all_guids_checks_passed, non_unique_guids = ( + CheckLogicBase.run_check_guid_unique(ifc_file) + ) + self.assertEqual(all_guids_checks_passed, False, "Should be False") + + def test_check_guid_unique_specific_guid_return(self): + """test the guid return of a failed GUID uniqueness check + the following represent a project using the DummyPlugin + """ + self.test_dir = tempfile.TemporaryDirectory() + ifc_paths = { + IFCDomain.arch: self.ifc_file_fkz_SB55_DoubleAndNoneGUID, + } + self.project = Project.create(self.test_dir.name, ifc_paths, + plugin=PluginDummy, ) + # weather data path is mandatory and "mocking" is not working + # so use a central defintion of weather file + self.project.sim_settings.weather_file_path = self.weather_file_path() + self.project.sim_settings.ids_file_path = self.ids_file_path() + # put project.run into DebugDecisionHandler is need, otherwise the + # playground.state() is empty and ifc_files are not available + # default answer for decision questions + answers = ('Other', 'Other',) + handler = DebugDecisionHandler(answers) + handler.handle(self.project.run(cleanup=False)) + + ifc_files = self.project.playground.state['ifc_files'] + + predicted_result = ['25OWQvmXj5BPgyergP43tY', '1Oms875aH3Wg$9l65H2ZGw'] + for ifc_file in ifc_files: + # self.run_check_guid_unique(ifc_file) + all_guids_checks_passed, non_unique_guids = ( + CheckLogicBase.run_check_guid_unique(ifc_file) + ) + list_guids_non_unique = list(non_unique_guids.keys()) + self.assertEqual(list_guids_non_unique, predicted_result, + "Should be a list of 2 GUIDs") + + def test_run_check_guid_empty_fail(self): + """test the boolean of all GUIDs has a value check, check fails + the following represent a project using the DummyPlugin + """ + self.test_dir = tempfile.TemporaryDirectory() + ifc_paths = { + IFCDomain.arch: self.ifc_file_fkz_SB55_DoubleAndNoneGUID, + } + self.project = Project.create(self.test_dir.name, ifc_paths, + plugin=PluginDummy, ) + # weather data path is mandatory and "mocking" is not working + # so use a central defintion of weather file + self.project.sim_settings.weather_file_path = self.weather_file_path() + self.project.sim_settings.ids_file_path = self.ids_file_path() + # put project.run into DebugDecisionHandler is need, otherwise the + # playground.state() is empty and ifc_files are not available + # default answer for decision questions + answers = ('Other', 'Other',) + handler = DebugDecisionHandler(answers) + handler.handle(self.project.run(cleanup=False)) + + ifc_files = self.project.playground.state['ifc_files'] + + for ifc_file in ifc_files: + # self.run_check_guid_unique(ifc_file) + all_guids_filled_passed, empty_guids = ( + CheckLogicBase.run_check_guid_empty(ifc_file) + ) + self.assertEqual(all_guids_filled_passed, False, "Should be false") + + def test_run_check_guid_empty_pass(self): + """test the boolean of all GUIDs has a value check, check pass + the following represent a project using the DummyPlugin + """ + self.test_dir = tempfile.TemporaryDirectory() + ifc_paths = { + IFCDomain.arch: self.ifc_file_fkz_SB55, + } + self.project = Project.create(self.test_dir.name, ifc_paths, + plugin=PluginDummy, ) + # weather data path is mandatory and "mocking" is not working + # so use a central defintion of weather file + self.project.sim_settings.weather_file_path = self.weather_file_path() + self.project.sim_settings.ids_file_path = self.ids_file_path() + # put project.run into DebugDecisionHandler is need, otherwise the + # playground.state() is empty and ifc_files are not available + # default answer for decision questions + answers = ('Other', 'Other',) + handler = DebugDecisionHandler(answers) + handler.handle(self.project.run(cleanup=False)) + + ifc_files = self.project.playground.state['ifc_files'] + + for ifc_file in ifc_files: + # self.run_check_guid_unique(ifc_file) + all_guids_filled_passed, empty_guids = ( + CheckLogicBase.run_check_guid_empty(ifc_file) + ) + self.assertEqual(all_guids_filled_passed, True, "Should be true") + + def test_run_check_ifc_version_error(self): + """test the version check of ifc file, + check: there is an error regarding the ifc version + the following represent a project using the DummyPlugin defined above + """ + + self.test_dir = tempfile.TemporaryDirectory() + ifc_paths = { + IFCDomain.arch: self.ifc_file_fkz_ifc23, + } + self.project = Project.create(self.test_dir.name, ifc_paths, + plugin=PluginDummy, ) + # weather data path is mandatory and "mocking" is not working + # so use a central defintion of weather file + self.project.sim_settings.weather_file_path = self.weather_file_path() + self.project.sim_settings.ids_file_path = self.ids_file_path() + # put project.run into DebugDecisionHandler is need, otherwise the + # playground.state() is empty and ifc_files are not available + # default answer for decision questions + answers = ('Other', 'Other',) + handler = DebugDecisionHandler(answers) + handler.handle(self.project.run(cleanup=False)) + + ifc_files = self.project.playground.state['ifc_files'] + + for ifc_file in ifc_files: + check_error, ifc_version = ( + CheckLogicBase.run_check_ifc_version(ifc_file) + ) + self.assertEqual(check_error, True, + "Should be True (ifc file has wrong ifc version))" + ) + + def test_run_check_ifc_version_no_error(self): + """test the version check of ifc file, + check: there is no error regarding the ifc version + the following represent a project using the DummyPlugin defined above + """ + + self.test_dir = tempfile.TemporaryDirectory() + ifc_paths = { + IFCDomain.arch: self.ifc_file_fkz, + } + self.project = Project.create(self.test_dir.name, ifc_paths, + plugin=PluginDummy, ) + # weather data path is mandatory and "mocking" is not working + # so use a central defintion of weather file + self.project.sim_settings.weather_file_path = self.weather_file_path() + self.project.sim_settings.ids_file_path = self.ids_file_path() + # put project.run into DebugDecisionHandler is need, otherwise the + # playground.state() is empty and ifc_files are not available + # default answer for decision questions + answers = ('Other', 'Other',) + handler = DebugDecisionHandler(answers) + handler.handle(self.project.run(cleanup=False)) + + ifc_files = self.project.playground.state['ifc_files'] + + for ifc_file in ifc_files: + check_error, ifc_version = ( + CheckLogicBase.run_check_ifc_version(ifc_file) + ) + self.assertEqual(check_error, False, + "Should be False \ + (ifc file has fitting ifc version))" + ) + + +class TestCheckIFCIfctester(unittest.TestCase): + """Tests for function checking IFC files, which are based on IDS file + respectifly the library ifctester + + """ + def test_checkIFC_IDS_examples_minimal_fail(self): + """check ifctester is working correctly by do check with IDS and ifc + files from the IDS repo + + see: + https://github.com/buildingSMART/IDS/tree/development/Documentation/ + ImplementersDocumentation/TestCases/ids + """ + + ifc_file = ( + test_rsrc_path / 'ids/' + 'fail-a_minimal_ids_can_check_a_minimal_ifc_1_2.ifc' + ) + ids_file = ( + test_rsrc_path / + 'ids/fail-a_minimal_ids_can_check_a_minimal_ifc_1_2.ids' + ) + all_checks_passed = CheckIfc.run_ids_check_on_ifc(ifc_file, ids_file) + self.assertEqual(all_checks_passed, False, "Should be false") + + def test_checkIFC_IDS_examples_required_specifications_fail(self): + """check ifctester is working correctly by do check with IDS and ifc + files from the IDS repo + + see: + https://github.com/buildingSMART/IDS/tree/development/Documentation/ + ImplementersDocumentation/TestCases/ids + """ + + ifc_file = ( + test_rsrc_path / 'ids/fail-required_specifications' + '_need_at_least_one_applicable_entity_2_2.ifc' + ) + ids_file = ( + test_rsrc_path / 'ids/fail-required_specifications' + '_need_at_least_one_applicable_entity_2_2.ids' + ) + all_checks_passed = CheckIfc.run_ids_check_on_ifc(ifc_file, ids_file) + self.assertEqual(all_checks_passed, False, "Should be false") + + def test_checkIFC_IDS_examples_a_specification_pass(self): + """check ifctester is working correctly by do check with IDS and ifc + files from the IDS repo + + see: + https://github.com/buildingSMART/IDS/tree/development/Documentation/ + ImplementersDocumentation/TestCases/ids + """ + + ifc_file = ( + test_rsrc_path / 'ids/pass-a_specification_passes_only_' + 'if_all_requirements_pass_2_2.ifc' + ) + ids_file = ( + test_rsrc_path / 'ids/pass-a_specification_passes_only_' + 'if_all_requirements_pass_2_2.ids' + ) + all_checks_passed = CheckIfc.run_ids_check_on_ifc(ifc_file, ids_file) + self.assertEqual(all_checks_passed, True, "Should be true") + + @unittest.skip("issue in ifcTester returns wrong results") + def test_checkIFC_IDS_examples_specification_optionality_pass(self): + """check ifctester is working correctly by do check with IDS and ifc + files from the IDS repo + + see: + https://github.com/buildingSMART/IDS/tree/development/Documentation/ + ImplementersDocumentation/TestCases/ids + + date: 2026-03-04 + this test fail, because there is potentially a bug in IDS itself, see + https://github.com/buildingSMART/IDS/issues/402 + remove this passage, when fixed + """ + + ifc_file = ( + test_rsrc_path / 'ids/pass-specification_optionality_and_' + 'facet_optionality_can_be_combined.ifc' + ) + ids_file = ( + test_rsrc_path / 'ids/pass-specification_optionality_and_facet_' + 'optionality_can_be_combined.ids' + ) + all_checks_passed = CheckIfc.run_ids_check_on_ifc(ifc_file, ids_file) + self.assertEqual(all_checks_passed, True, + "Should be true, fails because of issue in ifcTester,\ + 2025-03-12") + + def test_checkIFC_IDS_guid_length_22_pass(self): + """check ifctester for use case guid/GlobalID length = 22 character + """ + ifc_file = (Path(bim2sim.__file__).parent.parent / + 'test/resources/arch/ifc/AC20-FZK-Haus_with_SB55.ifc') + ids_file = ( + Path(bim2sim.__file__).parent.parent / + 'test/resources/ids/check_guid_length_equals_22_character.ids') + all_checks_passed = CheckIfc.run_ids_check_on_ifc(ifc_file, ids_file) + self.assertEqual(all_checks_passed, True, "Should be true") + + def test_checkIFC_IDS_guid_length_22_fail(self): + """check ifctester for use case guid/GlobalID length = 22 character + """ + ifc_file = ( + Path(bim2sim.__file__).parent.parent / + 'test/' + 'resources/arch/ifc/AC20-FZK-Haus_with_SB55_NoneAndDoubleGUID.ifc') + ids_file = ( + Path(bim2sim.__file__).parent.parent / + 'test/resources/ids/check_guid_length_equals_22_character.ids') + all_checks_passed = CheckIfc.run_ids_check_on_ifc(ifc_file, ids_file) + self.assertEqual(all_checks_passed, False, "Should be true") + + def test_checkIFC_IDS_2LSB_pass(self): + """check ifctester for use case 2nd Level Space Boundarys + """ + ifc_file = ( + Path(bim2sim.__file__).parent.parent / + 'test/resources/arch/ifc/AC20-FZK-Haus_with_SB55.ifc') + ids_file = ( + Path(bim2sim.__file__).parent.parent / + 'test/resources/ids/check_2LSB.ids') + all_checks_passed = CheckIfc.run_ids_check_on_ifc(ifc_file, ids_file) + self.assertEqual(all_checks_passed, True, "Should be true") + + def test_checkIFC_IDS_2LSB_fail(self): + """check ifctester for use case 2nd Level Space Boundarys + """ + ifc_file = ( + Path(bim2sim.__file__).parent.parent / + 'test/resources/arch/ifc/AC20-FZK-Haus.ifc') + ids_file = ( + Path(bim2sim.__file__).parent.parent / + 'test/resources/ids/check_2LSB.ids') + all_checks_passed = CheckIfc.run_ids_check_on_ifc(ifc_file, ids_file) + self.assertEqual(all_checks_passed, False, "Should be false") + + +if __name__ == "__main__": + unittest.main() diff --git a/bim2sim/plugins/__init__.py b/bim2sim/plugins/__init__.py index 268be134d5..3c7026a214 100644 --- a/bim2sim/plugins/__init__.py +++ b/bim2sim/plugins/__init__.py @@ -64,7 +64,6 @@ class PluginBPSBase(Plugin): sim_settings = BuildingSimSettings default_tasks = [ common.load_ifc.LoadIFC, - common.CheckIfc, common.create_elements, bps.CreateSpaceBoundaries, bps.DisaggregationCreationAndTypeCheck, diff --git a/bim2sim/tasks/checks/__init__.py b/bim2sim/tasks/checks/__init__.py new file mode 100644 index 0000000000..72938db6a8 --- /dev/null +++ b/bim2sim/tasks/checks/__init__.py @@ -0,0 +1,3 @@ +"""This task group contain prepocessing tasks +""" +from .check_ifc import CheckIfc, CheckLogicBPS, CheckLogicHVAC diff --git a/bim2sim/tasks/checks/check_ifc.py b/bim2sim/tasks/checks/check_ifc.py new file mode 100644 index 0000000000..ad46099fb7 --- /dev/null +++ b/bim2sim/tasks/checks/check_ifc.py @@ -0,0 +1,1508 @@ +"""Check ifc input file mainly based on IDS files.""" + +import inspect # used for _get_ifc_type_classes +import types # used for _get_class_property_sets +import os +import warnings + +from pathlib import Path + +from typing import Callable, Dict # Dict used for _get_class_property_sets + +import ifcopenshell as ifcos +from bim2sim.utilities.common_functions import all_subclasses # used in _get_ifc_type_classes +from bim2sim.elements.mapping import attribute # used in _get_ifc_type_classes +# get_layer_ifc needed for _check_inst_materials +from bim2sim.elements.mapping.ifc2python import get_layers_ifc, \ + get_property_sets, get_ports + +import ifctester.ids +import ifctester.reporter +import webbrowser + + +from mako.lookup import TemplateLookup +from mako.template import Template + +from bim2sim.elements import bps_elements as bps, hvac_elements as hvac +from bim2sim.tasks.base import ITask, Playground + +from bim2sim.kernel.ifc_file import IfcFileClass +from bim2sim.utilities.types import IFCDomain +from bim2sim import __file__ as bs_file +from bim2sim.tasks.common.load_ifc import extract_ifc_file_names + + +class CheckIfc(ITask): + """Check ifc files for their quality regarding simulation.""" + + reads = ('ifc_files',) + + def __init__(self, playground: Playground): + """Initialize CheckIFC.""" + super().__init__(playground) + self.error_summary_sub_inst: dict = {} + self.error_summary_inst: dict = {} + self.error_summary_prop: dict = {} + self.version_error: bool = False + self.ifc_version: str = None + self.all_guids_unique: bool = True + self.double_guids: dict = {} + self.all_guids_filled: bool = True + self.empty_guids: dict = {} + self.sub_inst: list = [] + self.id_list: list = [] + self.elements: list = [] + self.ps_summary: dict = {} + self.ifc_units: dict = {} + self.sub_inst_cls = None + self.plugin = None + + def run(self, ifc_files: [IfcFileClass]): + """ + Analyzes sub_elements and elements of an IFC file. + + Therefore validation functions check ifc files and export the errors + found as .html files. + + It creates following reports: + error_summary: overview of all errors + error_summary_inst: summary of errors related to elements + error_summary_prop: summary of missing properties + error_summary_guid: summary of GUID errors + ifc_ids_check: results of checks based on IDS file + These html files are stored in the log folder of the project folder. + + Args: + ifc_files: bim2sim IfcFileClass holding the ifcopenshell ifc + instance + """ + self.logger.info("Processing IFC Checks with ifcTester") + + base_path = self.paths.ifc_base + + ifc_files_paths = extract_ifc_file_names(base_path) + self.logger.info(f"Found {len(ifc_files_paths)} IFC files in project " + f"directory.") + + log_path = self.paths.log + # ids check call start + if self.playground.sim_settings.ids_file_path is None: + self.logger.critical("Default ids file is used, pls set " + + "project.sim_settings.ids_file_path!") + self.playground.sim_settings.ids_file_path = ( + Path(bs_file).parent / + 'plugins/PluginIFCCheck/bim2sim_ifccheck/ifc_bps.ids' + ) + + ids_file_path = self.playground.sim_settings.ids_file_path + for ifc_file_path in ifc_files_paths: + all_spec_pass = self.run_ids_check_on_ifc( + ifc_file_path, ids_file_path, + report_html=True, log_path=log_path) + + if all_spec_pass: + self.logger.info( + "all checks of the specifications of this IDS pass: " + + "{}".format(all_spec_pass)) + else: + self.logger.warning( + "all checks of the specifications of this IDS pass: " + + "{}".format(all_spec_pass)) + # ids check call end + + self.logger.info("Processing IFC Checks without ifcTester") + + paths = self.paths + for ifc_file in ifc_files: + # checks are domain specific + # Reset class based on domain to run the right check. + # Not pretty but works. This might be refactored in #170 + + # check uniqueness of GUIDs + self.all_guids_unique, self.double_guids = ( + CheckLogicBase.run_check_guid_unique(ifc_file) + ) + list_guids_non_unique = list(self.double_guids.keys()) + self.logger.info("the GUIDs of all elements are unique: " + + "{}".format(self.all_guids_unique)) + if self.all_guids_unique is False: + self.logger.critical("non-unique GUIDs: " + + "{}".format(list_guids_non_unique)) + # check emptiness of GUID fields + self.all_guids_filled, self.empty_guids = ( + CheckLogicBase.run_check_guid_empty(ifc_file) + ) + list_guids_empty = list(self.empty_guids.keys()) + self.logger.info("the GUIDs of all elements are filled " + + "(NOT empty): {}".format(self.all_guids_filled)) + if self.all_guids_filled is False: + self.logger.critical("empty GUIDs: {}".format(list_guids_empty)) + # check ifc version + self.version_error, self.ifc_version = ( + CheckLogicBase.run_check_ifc_version(ifc_file) + ) + # for doc string + # Logs: + # critical: if loaded IFC is not IFC4 + if self.version_error: + self.logger.critical("ifc Version is not fitting. " + + "Should be IFC4, but here: " + + self.ifc_version) + + if ifc_file.domain == IFCDomain.hydraulic: + self.logger.info("Processing HVAC-IfcCheck") + # used for data preparing, it is a filter keyword + self.sub_inst_cls = 'IfcDistributionPort' + self.plugin = hvac + self.ps_summary = self._get_class_property_sets(self.plugin) + self.sub_inst = ifc_file.file.by_type(self.sub_inst_cls) + self.elements = self.get_relevant_elements(ifc_file.file) + self.ifc_units = ifc_file.ifc_units + # checking itself + check_logic_hvac = CheckLogicHVAC( + self.sub_inst, self.elements, self.ps_summary, + self.ifc_units) + self.error_summary_sub_inst = check_logic_hvac.check_inst_sub() + self.error_summary_inst = check_logic_hvac.check_elements() + + elif ifc_file.domain == IFCDomain.arch: + self.logger.info("Processing BPS-IfcCheck") + # used for data preparing, it is a filter keyword + self.sub_inst_cls = 'IfcRelSpaceBoundary' + self.plugin = bps + self.ps_summary = self._get_class_property_sets(self.plugin) + self.sub_inst = ifc_file.file.by_type(self.sub_inst_cls) + self.elements = self.get_relevant_elements(ifc_file.file) + self.ifc_units = ifc_file.ifc_units + # checking itself + check_logic_bps = CheckLogicBPS( + self.sub_inst, self.elements, self.ps_summary, + self.ifc_units) + self.error_summary_sub_inst = check_logic_bps.check_inst_sub() + self.error_summary_inst = check_logic_bps.check_elements() + self.error_summary_prop = check_logic_bps.error_summary_prop + self.paths = paths + elif ifc_file.domain == IFCDomain.unknown: + self.logger.info(f"No domain specified for ifc file " + f"{ifc_file.ifc_file_name}, not processing " + f"any checks") + return + else: + self.logger.info( + f"For the Domain {ifc_file.domain} no specific checks are" + f" implemented currently. Just running the basic checks." + f"") + + # generating reports (of the additional checks) + base_name = f"/{ifc_file.domain.name.upper()}_" \ + f"{ifc_file.ifc_file_name[:-4]}" + self._write_errors_to_html_table(base_name, ifc_file.domain) + + def get_relevant_elements(self, ifc: ifcos.file): + """Get all relevant ifc elements. + + This function based on the plugin's classes that + represent an IFCProduct. + + Args: + ifc: IFC file translated with ifcopenshell + + Returns: + ifc_elements: list of IFC instance (Products) + """ + relevant_ifc_types = list(self.ps_summary.keys()) + ifc_elements = [] + for ifc_type in relevant_ifc_types: + ifc_elements.extend(ifc.by_type(ifc_type)) + return ifc_elements + + @staticmethod + def _get_ifc_type_classes(plugin: types.ModuleType): + """Get all the classes of a plugin that represent an IFCProduct. + + Furthermore, organize them on a dictionary for each ifc_type. + Args: + plugin: plugin used in the check tasks (bps or hvac) + + Returns: + cls_summary: dictionary containing all the ifc_types on the + plugin with the corresponding class + """ + plugin_classes = [plugin_class[1] for plugin_class in + inspect.getmembers(plugin, inspect.isclass) if + inspect.getmro(plugin_class[1])[1].__name__.endswith( + 'Product')] + cls_summary = {} + + for plugin_class in plugin_classes: + # class itself + if plugin_class.ifc_types: + for ifc_type in plugin_class.ifc_types.keys(): + cls_summary[ifc_type] = plugin_class + # sub classes + for subclass in all_subclasses(plugin_class): + for ifc_type in subclass.ifc_types.keys(): + cls_summary[ifc_type] = subclass + return cls_summary + + @classmethod + def _get_class_property_sets(cls, plugin: types.ModuleType) -> Dict: + """Get all property sets and properties. + + Which are required for bim2sim for all classes of a plugin, that + represent an IFCProduct, and organize them on a dictionary for each + ifc_type Args: plugin: plugin used in the check tasks (bps or hvac) + + Returns: + ps_summary: dictionary containing all the ifc_types on the + plugin with the corresponding property sets + + """ + ps_summary = {} + cls_summary = cls._get_ifc_type_classes(plugin) + for ifc_type, plugin_class in cls_summary.items(): + attributes = inspect.getmembers( + plugin_class, lambda a: isinstance(a, attribute.Attribute)) + ps_summary[ifc_type] = {} + for attr in attributes: + if attr[1].default_ps: + ps_summary[ifc_type][attr[0]] = attr[1].default_ps + return ps_summary + + def validate_sub_inst(self, sub_inst: list) -> list: + """Raise NotImplemented Error.""" + raise NotImplementedError + + @staticmethod + def run_ids_check_on_ifc(ifc_file: str, ids_file: str, + report_html: bool = False, + log_path: str = None) -> bool: + """Run check on IFC file based on IDS. + + print the check of specifications pass(true) or fail(false) + and the name of the specification + and if all specifications of one IDS pass + + Args: + ifc_file: path of the IFC file, which is checked + ids_file: path of the IDS file, which includes the specifications + log_path: path of the log folder as part of the project structure + report_html: generate, save and open the report about checking + default = False + Returns: + all_spec_pass: boolean + (true: all specification passed, + false: one or more specification not passed) + """ + model = ifcos.open(ifc_file) + my_ids = ifctester.ids.open(ids_file) + my_ids.validate(model) + all_spec_pass = True + for spec in my_ids.specifications: + if not spec.status: + all_spec_pass = False + + # generate html report + if report_html: + engine = ifctester.reporter.Html(my_ids) + engine.report() + output_file = Path(log_path / 'ifc_ids_check.html') + engine.to_file(output_file) + # can comment out, if not the browser should show the report + # webbrowser.open(f"file://{output_file}") + + return all_spec_pass + + def get_html_templates(self): + """Get all stored html templates. + + Which will be used to export the errors summaries. + + Returns: + templates: dictionary containing all error html templates + """ + templates = {} + path_templates = os.path.join( + self.paths.assets, "templates", "check_ifc") + lookup = TemplateLookup(directories=[path_templates]) + templates["inst_template"] = Template( + filename=os.path.join(path_templates, "inst_template"), + lookup=lookup) + templates["prop_template"] = Template( + filename=os.path.join(path_templates, "prop_template"), + lookup=lookup) + templates["summary_template"] = Template( + filename=os.path.join(path_templates, "summary_template_extend"), + lookup=lookup) + templates["guid_template"] = Template( + filename=os.path.join(path_templates, "guid_template"), + lookup=lookup) + return templates + + @staticmethod + def _categorize_errors(error_dict: dict): + """Categorizes the resulting errors in a dictionary. + + This dictionary contains two groups: + 'per_error' where the key is the error name and the value is the + number of errors with this name + 'per type' where the key is the ifc_type and the values are the + each element with its respective errors + Args: + error_dict: dictionary containing all errors without categorization + + Returns: + categorized_dict: dictionary containing all errors categorized + """ + categorized_dict = {'per_error': {}, 'per_type': {}} + for instance, errors in error_dict.items(): + if ' ' in instance: + guid, ifc_type = instance.split(' ') + else: + guid = '-' + ifc_type = instance + if ifc_type not in categorized_dict['per_type']: + categorized_dict['per_type'][ifc_type] = {} + categorized_dict['per_type'][ifc_type][guid] = errors + for error in errors: + error_com = error.split(' - ') + if error_com[0] not in categorized_dict['per_error']: + categorized_dict['per_error'][error_com[0]] = 0 + categorized_dict['per_error'][error_com[0]] += 1 + return categorized_dict + + def _write_errors_to_html_table(self, base_name: str, domain: IFCDomain): + """Write all errors in the html templates in a summarized way. + + Args: + base_name: str of file base name for reports + domain: IFCDomain of the checked IFC + """ + show_report = False # enable the automatic popup of the reports + templates = self.get_html_templates() + summary_inst = self._categorize_errors(self.error_summary_inst) + summary_sbs = self._categorize_errors(self.error_summary_sub_inst) + summary_props = self._categorize_errors(self.error_summary_prop) + all_errors = {**summary_inst['per_type'], **summary_sbs['per_type']} + + with open(str(self.paths.log) + + base_name + + '_error_summary_inst.html', 'w+') as \ + out_file: + out_file.write(templates["inst_template"].render_unicode( + task=self, + summary_inst=summary_inst, + summary_sbs=summary_sbs, + all_errors=all_errors)) + out_file.close() + # opens automatically browser tab showing the generated html report + if show_report: + webbrowser.open(f"file://{out_file.buffer.name}") + with open(str(self.paths.log) + + base_name + + '_error_summary_prop.html', 'w+') as \ + out_file: + out_file.write(templates["prop_template"].render_unicode( + task=self, + summary_props=summary_props)) + out_file.close() + # opens automatically browser tab showing the generated html report + if show_report: + webbrowser.open(f"file://{out_file.buffer.name}") + with open(str(self.paths.log) + + base_name + + '_error_summary.html', 'w+') as out_file: + out_file.write(templates["summary_template"].render_unicode( + ifc_version=self.ifc_version, + version_error=self.version_error, + all_guids_unique=self.all_guids_unique, + double_guids=self.double_guids, + all_guids_filled=self.all_guids_filled, + empty_guids=self.empty_guids, + task=self, + plugin_name=domain.name.upper(), + base_name=base_name[1:], + summary_inst=summary_inst, + summary_sbs=summary_sbs, + summary_props=summary_props)) + out_file.close() + # opens automatically browser tab showing the generated html report + if show_report: + webbrowser.open(f"file://{out_file.buffer.name}") + + with open(str(self.paths.log) + base_name + '_error_summary_guid.html', + 'w+') as \ + out_file: + out_file.write(templates["guid_template"].render_unicode( + task=self, + double_guids=self.double_guids, + empty_guids=self.empty_guids, + summary_inst=summary_inst, + summary_sbs=summary_sbs, + all_errors=all_errors)) + out_file.close() + # opens automatically browser tab showing the generated html report + if show_report: + webbrowser.open(f"file://{out_file.buffer.name}") + + +class CheckLogicBase(): + """Provides logic for ifc files checking regarding simulation. + + This is a base class. This base class includes all check logic, which is + useful for all checking use cases. + + Attributes: + extract_data (list): filtered/extract data from ifc file + """ + + def __init__(self, extract_data, elements, ps_summary, ifc_units): + """Initialize class.""" + self.space_ndicator = True + # filtered data, which will be processed + self.extract_data = extract_data + self.elements = elements + self.ps_summary = ps_summary + self.ifc_units = ifc_units + self.error_summary_prop: dict = {} + + @staticmethod + def run_check_guid_unique(ifc_file) -> (bool, dict): + """Check the uniqueness of the guids of the IFC file. + + Here the bijective uniqueness is check, but also + the uniqueness of modified guids by transforming + the lowercase letters into uppercase letter + Args: + ifc_file: path of the IFC file, which is checked + + Returns: + all_guids_unique: boolean + (true: all guids are unique + false: one or more guids are not unique) + + double_guid: dict + + """ + # dict of all elements with guids used in the checked ifc model + used_guids: dict[str, ifcos.entity_instance] = dict() + # dict of elements with guids, which are not unique + double_guids: dict[str, ifcos.entity_instance] = dict() + all_guids_unique = True + used_guids_upper = [] # to store temporally guid in uppercase letters + for inst in ifc_file.file: + if hasattr(inst, "GlobalId"): + guid = inst.GlobalId + # print(guid) + upper_guid = guid.upper() + # print(upper_guid) + if guid in used_guids: + double_guids[guid] = inst + all_guids_unique = False + warnings.warn( + "Some GUIDs are not unique! A bijective ifc file have " + "to have unique GUIDs. But bim2sim provides an option " + "in sim_settings: rest_guids = True" + ) + elif (guid.upper() in used_guids_upper): + double_guids[guid] = inst + all_guids_unique = False + warnings.warn( + "Some GUIDs are not unique (for transformed GUIDS " + "letters low-case into uppercase)! " + "A bijective ifc file have " + "to have unique GUIDs. But bim2sim provides an option " + "in sim_settings: rest_guids = True" + ) + else: + used_guids[guid] = inst + # store temporally guid in uppercase letters + used_guids_upper.append(upper_guid) + return (all_guids_unique, double_guids) + + @staticmethod + def run_check_guid_empty(ifc_file) -> (bool, dict): + """Check it there is/are guid/s, which is/are empty in the IFC file. + + Args: + ifc_file: path of the IFC file, which is checked + + Returns: + all_guids_filled: boolean + (true: all guids has a value (not empty) + false: one or more guids has not value (empty)) + + empty_guid: dict + """ + # dict of all elements with guids used in the checked ifc model + used_guids: dict[str, ifcos.entity_instance] = dict() + # dict of elements with guids, which are empty + empty_guids: dict[str, ifcos.entity_instance] = dict() + all_guids_filled = True + # count the number of guids without value (empty), this number is used + # to make unique identifier + guid_empty_no = 0 + for inst in ifc_file.file: + if hasattr(inst, "GlobalId"): + guid = inst.GlobalId + name = inst.Name + if guid == '': + all_guids_filled = False + guid_empty_no = guid_empty_no + 1 + name_dict = name + '--' + str(guid_empty_no) + empty_guids[name_dict] = inst + else: + used_guids[guid] = inst + return (all_guids_filled, empty_guids) + + @staticmethod + def run_check_ifc_version(ifc: ifcos.file) -> (bool, str): + """Check the IFC version. + + Only IFC4 files are valid for bim2sim. + + Attention: no Error is raised anymore. + + Args: + ifc: ifc file loaded with IfcOpenShell + Returns: + version_error: True if version NOT fit + ifc_version: version of the ifc file + """ + schema = ifc.schema + if "IFC4" not in schema: + version_error = True + else: + version_error = False + return (version_error, schema) + + def check_inst_sub(self): + """Check sub instances for errors. + + Based on functions in validate_sub_inst via check_inst + """ + error_summary_sub_inst = self.check_inst( + self.validate_sub_inst, self.extract_data) + return error_summary_sub_inst + + def check_elements(self): + """Check elements for errors. + + Based on functions in validate_sub_inst via check_inst + """ + error_summary = self.check_inst( + self.validate_elements, self.elements) + return error_summary + + @staticmethod + def check_inst(validation_function: Callable, elements: list): + """Check each element lists. + + Use sb_validation/ports/elements functions to check elements + adds error to dictionary if object has errors. Combines the + (error) return of the specific validation function with the + key (mostly the GlobalID). + + Args: + validation_function: function that compiles all the validations + to be performed on the object + (sb/port/instance) + elements: list containing all objects to be evaluates + + Returns: + summary: summarized dictionary of errors, where the key is the + GUID + the ifc_type + + """ + summary = {} + for inst in elements: + error = validation_function(inst) + if len(error) > 0: + if hasattr(inst, 'GlobalId'): + key = inst.GlobalId + ' ' + inst.is_a() + else: + key = inst.is_a() + summary.update({key: error}) + return summary + + @staticmethod + def apply_validation_function(fct: bool, err_name: str, error: list): + """Apply a validation to an instance, space boundary or port. + + Function to apply a validation to an instance, space boundary or + port, it stores the error to the list of errors. + + Args: + fct: validation function to be applied + err_name: string that define the error + error: list of errors + + """ + if not fct: + error.append(err_name) + + @staticmethod + def _check_rel_space(bound: ifcos.entity_instance): + """Check the existence of related space. + + Args: + bound: Space boundary IFC instance + + Returns: + True: if check succeeds + False: if check fails + """ + return any( + [bound.RelatingSpace.is_a('IfcSpace') or + bound.RelatingSpace.is_a('IfcExternalSpatialElement')]) + + def validate_sub_inst(self, sub_inst: list) -> list: + """Raise NotImplemented Error.""" + raise NotImplementedError + + +class CheckLogicBPS(CheckLogicBase): + """Provides additional logic for ifc files checking regarding BPS.""" + + @staticmethod + def _check_rel_space(bound: ifcos.entity_instance): + """Check existence of related space. + + And has the correctness of class type. + + Args: + bound: Space boundary IFC instance + + Returns: + True: if check succeeds + False: if check fails + """ + return any( + [bound.RelatingSpace.is_a('IfcSpace') or + bound.RelatingSpace.is_a('IfcExternalSpatialElement')]) + + @staticmethod + def _check_rel_building_elem(bound: ifcos.entity_instance): + """Check existence of related building element. + + And the correctness of class type. + + Args: + bound: Space boundary IFC instance + + Returns: + True: if check succeeds + False: if check fails + """ + if bound.RelatedBuildingElement is not None: + return bound.RelatedBuildingElement.is_a('IfcElement') + + @staticmethod + def _check_conn_geom(bound: ifcos.entity_instance): + """Check that the space boundary has a connection geometry. + + And the correctness of class type. + + Args: + bound: Space boundary IFC instance + + Returns: + True: if check succeeds + False: if check fails + """ + if bound.ConnectionGeometry is not None: + return bound.ConnectionGeometry.is_a('IfcConnectionGeometry') + + @staticmethod + def _check_on_relating_elem(bound: ifcos.entity_instance): + """Check geometric information. + + Check that the surface on relating element of a space boundary has the + geometric information and the correctness of class type. + + Args: + bound: Space boundary IFC instance + + Returns: + True: if check succeeds + False: if check fails + + """ + if bound.ConnectionGeometry is not None: + return bound.ConnectionGeometry.SurfaceOnRelatingElement.is_a( + 'IfcCurveBoundedPlane') + + @staticmethod + def _check_on_related_elem(bound: ifcos.entity_instance): + """Check absence of geometric information. + + Check that the surface on related element of a space boundary has no + geometric information and the correctness of class type. + + Args: + bound: Space boundary IFC instance + + Returns: + True: if check succeeds + False: if check fails + """ + if bound.ConnectionGeometry is not None: + return (bound.ConnectionGeometry.SurfaceOnRelatedElement is None or + bound.ConnectionGeometry.SurfaceOnRelatedElement.is_a( + 'IfcCurveBoundedPlane')) + + @staticmethod + def _check_basis_surface(bound: ifcos.entity_instance): + """Check representation by an IFC Place. + + Check that the surface on relating element of a space boundary is + represented by an IFC Place and the correctness of class type. + + Args: + bound: Space boundary IFC instance + + Returns: + True: if check succeeds + False: if check fails + """ + if bound.ConnectionGeometry is not None: + return bound.ConnectionGeometry.SurfaceOnRelatingElement. \ + BasisSurface.is_a('IfcPlane') + + @staticmethod + def _check_inner_boundaries(bound: ifcos.entity_instance): + """Check absence of the surface and their structure. + + Check if the surface on relating element of a space boundary inner + boundaries don't exists or are composite curves. + Args: + bound: Space boundary IFC instance + + Returns: + True: if check succeeds + False: if check fails + """ + if bound.ConnectionGeometry is not None: + return (bound.ConnectionGeometry.SurfaceOnRelatingElement. + InnerBoundaries is None) or \ + (i.is_a('IfcCompositeCurve') for i in bound. + ConnectionGeometry.SurfaceOnRelatingElement. + InnerBoundaries) + + @staticmethod + def _check_outer_boundary_composite(bound: ifcos.entity_instance): + """Check if the surface are composite curves. + + Check if the surface on relating element of a space boundary outer + boundaries are composite curves. + + Args: + bound: Space boundary IFC instance + + Returns: + True: if check succeeds + False: if check fails + """ + return bound.ConnectionGeometry.SurfaceOnRelatingElement. \ + OuterBoundary.is_a('IfcCompositeCurve') + + @staticmethod + def _check_segments(bound: ifcos.entity_instance): + """Check if the surface are poly-line. + + Check if the surface on relating element of a space boundary outer + boundaries segments are poly-line. + + Args: + bound: Space boundary IFC instance + + Returns: + True: if check succeeds + False: if check fails + """ + return (s.is_a('IfcCompositeCurveSegment') for s in + bound.ConnectionGeometry.SurfaceOnRelatingElement. + OuterBoundary.Segments) + + @staticmethod + def _check_coords(points: ifcos.entity_instance): + """Check coordinates of a group of points (class and length). + + Args: + points: Points IFC instance + + Returns: + True: if check succeeds + False: if check fails + """ + return points.is_a('IfcCartesianPoint') and 1 <= len( + points.Coordinates) <= 4 + + @staticmethod + def _check_dir_ratios(dir_ratios: ifcos.entity_instance): + """Check length of direction ratios. + + Args: + dir_ratios: direction ratios IFC instance + + Returns: + True: if check succeeds + False: if check fails + """ + return 2 <= len(dir_ratios.DirectionRatios) <= 3 + + @classmethod + def _check_poly_points_coord(cls, polyline: ifcos.entity_instance): + """Check if a poly-line has the correct coordinates. + + Args: + polyline: Poly-line IFC instance + + Returns: + True: if check succeeds + False: if check fails + """ + return all(cls._check_coords(p) for p in polyline.Points) + + @classmethod + def _check_segments_poly_coord(cls, bound: ifcos.entity_instance): + """Check segments coordinates. + + Check segments coordinates of an outer boundary of a surface on + relating element. + + Args: + bound: Space boundary IFC instance + + Returns: + True: if check succeeds + False: if check fails + """ + return all(cls._check_poly_points_coord(s.ParentCurve) + for s in + bound.ConnectionGeometry.SurfaceOnRelatingElement. + OuterBoundary.Segments) + + @staticmethod + def _check_poly_points(polyline: ifcos.entity_instance): + """Check if a polyline has the correct class. + + Args: + polyline: Polyline IFC instance + + Returns: + True: if check succeeds + False: if check fails + """ + return polyline.is_a('IfcPolyline') + + @classmethod + def _check_outer_boundary_poly(cls, bound: ifcos.entity_instance): + """Check points of outer boundary of a surface on relating element. + + Args: + bound: Space boundary IFC instance + + Returns: + True: if check succeeds + False: if check fails + """ + return cls._check_poly_points( + bound.ConnectionGeometry.SurfaceOnRelatingElement.OuterBoundary) + + @staticmethod + def _check_outer_boundary_poly_coord(bound: ifcos.entity_instance): + """Check outer boundary of a surface on relating element. + + Args: + bound: Space boundary IFC instance + + Returns: + True: if check succeeds + False: if check fails + """ + return all( + bound.ConnectionGeometry.SurfaceOnRelatingElement.OuterBoundary) + + @staticmethod + def _check_plane_position(bound: ifcos.entity_instance): + """ + Check class of plane position of space boundary. + + Args: + bound: Space boundary IFC instance + + Returns: + True: if check succeeds + False: if check fails + """ + return bound.ConnectionGeometry.SurfaceOnRelatingElement.BasisSurface. \ + Position.is_a('IfcAxis2Placement3D') + + @staticmethod + def _check_location(bound: ifcos.entity_instance): + """ + Check that location of a space boundary is an IfcCartesianPoint. + + Args: + bound: Space boundary IFC instance + + Returns: + True: if check succeeds + False: if check fails + """ + return bound.ConnectionGeometry.SurfaceOnRelatingElement.BasisSurface. \ + Position.Location.is_a('IfcCartesianPoint') + + @staticmethod + def _check_axis(bound: ifcos.entity_instance): + """Check that axis of space boundary is an IfcDirection. + + Args: + bound: Space boundary IFC instance + + Returns: + True: if check succeeds + False: if check fails + """ + return bound.ConnectionGeometry.SurfaceOnRelatingElement.BasisSurface. \ + Position.Axis.is_a('IfcDirection') + + @staticmethod + def _check_refdirection(bound: ifcos.entity_instance): + """Check that reference direction of space boundary is an IfcDirection. + + Args: + bound: Space boundary IFC instance + + Returns: + True: if check succeeds + False: if check fails + """ + return bound.ConnectionGeometry.SurfaceOnRelatingElement.BasisSurface. \ + Position.RefDirection.is_a('IfcDirection') + + @classmethod + def _check_location_coord(cls, bound: ifcos.entity_instance): + """Check the correctness of related coordinates. + + Check if space boundary surface on relating element coordinates are + correct. + + Args: + bound: Space boundary IFC instance + + Returns: + True: if check succeeds + False: if check fails + + """ + return cls._check_coords(bound.ConnectionGeometry. + SurfaceOnRelatingElement.BasisSurface. + Position.Location) + + @classmethod + def _check_axis_dir_ratios(cls, bound: ifcos.entity_instance): + """Check correctness of space boundary surface. + + Check if space boundary surface on relating element axis are correct. + + Args: + bound: Space boundary IFC instance + + Returns: + True: if check succeeds + False: if check fails + """ + return cls._check_dir_ratios( + bound.ConnectionGeometry.SurfaceOnRelatingElement.BasisSurface. + Position.Axis) + + @classmethod + def _check_refdirection_dir_ratios(cls, bound: ifcos.entity_instance): + """Check correctness of space boundary surface. + + Check if space boundary surface on relating element reference direction + are correct. + + Args: + bound: Space boundary IFC instance + + Returns: + True: if check succeeds + False: if check fails + """ + return cls._check_dir_ratios( + bound.ConnectionGeometry.SurfaceOnRelatingElement.BasisSurface. + Position.RefDirection) + + @staticmethod + def _check_inst_sb(inst: ifcos.entity_instance): + """Check association of an instance. + + Check that an instance has associated space boundaries (space or + building element). + + Args: + inst: IFC instance + + Returns: + True: if check succeeds + False: if check fails + """ + blacklist = ['IfcBuilding', 'IfcSite', 'IfcBuildingStorey', + 'IfcMaterial', 'IfcMaterialLayer', 'IfcMaterialLayerSet'] + if inst.is_a() in blacklist: + return True + elif inst.is_a('IfcSpace') or inst.is_a('IfcExternalSpatialElement'): + return len(inst.BoundedBy) > 0 + else: + if len(inst.ProvidesBoundaries) > 0: + return True + decompose = [] + if hasattr(inst, 'Decomposes') and len(inst.Decomposes): + decompose = [decomp.RelatingObject for decomp in + inst.Decomposes] + elif hasattr(inst, 'IsDecomposedBy') and len(inst.IsDecomposedBy): + decompose = [] + for decomp in inst.IsDecomposedBy: + for inst_ifc in decomp.RelatedObjects: + decompose.append(inst_ifc) + for inst_decomp in decompose: + if len(inst_decomp.ProvidesBoundaries): + return True + return False + + @staticmethod + def _check_inst_materials(inst: ifcos.entity_instance): + """Check that an instance has associated materials. + + Args: + inst: IFC instance + + Returns: + True: if check succeeds + False: if check fails + """ + blacklist = [ + 'IfcBuilding', 'IfcSite', 'IfcBuildingStorey', 'IfcSpace', + 'IfcExternalSpatialElement'] + if not (inst.is_a() in blacklist): + return len(get_layers_ifc(inst)) > 0 + return True + + def _check_inst_properties(self, inst: ifcos.entity_instance): + """Check existence of necessary property sets and properties. + + Check that an instance has the property sets and properties + necessaries to the plugin. + Args: + inst: IFC instance + + Returns: + True: if check succeeds + False: if check fails + """ + inst_prop2check = self.ps_summary.get(inst.is_a(), {}) + inst_prop = get_property_sets(inst, self.ifc_units) + inst_prop_errors = [] + for prop2check, ps2check in inst_prop2check.items(): + ps = inst_prop.get(ps2check[0], None) + if ps: + if not ps.get(ps2check[1], None): + inst_prop_errors.append( + prop2check+' - '+', '.join(ps2check)) + else: + inst_prop_errors.append(prop2check+' - '+', '.join(ps2check)) + if inst_prop_errors: + key = inst.GlobalId + ' ' + inst.is_a() + self.error_summary_prop.update({key: inst_prop_errors}) + return False + return True + + @staticmethod + def _check_inst_contained_in_structure(inst: ifcos.entity_instance): + """Check that an instance is contained in an structure. + + Args: + inst: IFC instance + + Returns: + True: if check succeeds + False: if check fails + """ + blacklist = [ + 'IfcBuilding', 'IfcSite', 'IfcBuildingStorey', 'IfcSpace', + 'IfcExternalSpatialElement', 'IfcMaterial', 'IfcMaterialLayer', + 'IfcMaterialLayerSet' + ] + if not (inst.is_a() in blacklist): + return len(inst.ContainedInStructure) > 0 + if hasattr(inst, 'Decomposes'): + return len(inst.Decomposes) > 0 + else: + return True + + @staticmethod + def _check_inst_representation(inst: ifcos.entity_instance): + """ + Check that an instance has a correct geometric representation. + + Args: + inst: IFC instance + + Returns: + True: if check succeeds + False: if check fails + """ + blacklist = [ + 'IfcBuilding', 'IfcBuildingStorey', 'IfcMaterial', + 'IfcMaterialLayer', 'IfcMaterialLayerSet' + ] + if not (inst.is_a() in blacklist): + return inst.Representation is not None + return True + + def validate_sub_inst(self, bound: ifcos.entity_instance) -> list: + """Validate space boundary. + + Validation function for a space boundary that compiles all validation + functions. + + Args: + bound: ifc space boundary entity + + Returns: + error: list of errors found in the ifc space boundaries + """ + error = [] + # print(bound) + self.apply_validation_function(self._check_rel_space(bound), + 'RelatingSpace - ' + 'The space boundary does not have a ' + 'relating space associated', error) + self.apply_validation_function(self._check_rel_building_elem(bound), + 'RelatedBuildingElement - ' + 'The space boundary does not have a ' + 'related building element associated', + error) + self.apply_validation_function(self._check_conn_geom(bound), + 'ConnectionGeometry - ' + 'The space boundary does not have a ' + 'connection geometry', error) + self.apply_validation_function(self._check_on_relating_elem(bound), + 'SurfaceOnRelatingElement - ' + 'The space boundary does not have a ' + 'surface on the relating element', + error) + self.apply_validation_function(self._check_on_related_elem(bound), + 'SurfaceOnRelatedElement - ' + 'The space boundary does not have a ' + 'surface on the related element', error) + self.apply_validation_function(self._check_basis_surface(bound), + 'BasisSurface - ' + 'The space boundary surface on ' + 'relating element geometry is missing', + error) + self.apply_validation_function(self._check_inner_boundaries(bound), + 'InnerBoundaries - ' + 'The space boundary surface on ' + 'relating element inner boundaries are ' + 'missing', error) + if bound.ConnectionGeometry is not None: + if hasattr( + bound.ConnectionGeometry.SurfaceOnRelatingElement. + OuterBoundary, 'Segments'): + self.apply_validation_function( + self._check_outer_boundary_composite(bound), + 'OuterBoundary - ' + 'The space boundary surface on relating element outer ' + 'boundary is missing', error) + self.apply_validation_function(self._check_segments(bound), + 'OuterBoundary Segments - ' + 'The space boundary surface on ' + 'relating element outer ' + 'boundary ' + 'geometry is missing', error) + self.apply_validation_function( + self._check_segments_poly_coord(bound), + 'OuterBoundary Coordinates - ' + 'The space boundary surface on relating element outer ' + 'boundary coordinates are missing', error) + else: + self.apply_validation_function( + self._check_outer_boundary_poly(bound), + 'OuterBoundary - ' + 'The space boundary surface on relating element outer ' + 'boundary is missing', error) + self.apply_validation_function( + self._check_outer_boundary_poly_coord(bound), + 'OuterBoundary Coordinates - ' + 'The space boundary surface on relating element outer ' + 'boundary coordinates are missing', error) + self.apply_validation_function(self._check_plane_position(bound), + 'Position - ' + 'The space boundary surface on' + 'relating ' + 'element plane position is missing', + error) + self.apply_validation_function(self._check_location(bound), + 'Location - ' + 'The space boundary surface on ' + 'relating element location is ' + 'missing', error) + self.apply_validation_function(self._check_axis(bound), + 'Axis - ' + 'The space boundary surface on' + 'relating ' + 'element axis are missing', + error) + self.apply_validation_function(self._check_refdirection(bound), + 'RefDirection - ' + 'The space boundary surface on ' + 'relating ' + 'element reference direction is ' + 'missing', error) + self.apply_validation_function(self._check_location_coord(bound), + 'LocationCoordinates - ' + 'The space boundary surface on' + 'relating ' + 'element location coordinates are ' + 'missing', error) + self.apply_validation_function(self._check_axis_dir_ratios(bound), + 'AxisDirectionRatios - ' + 'The space boundary surface on ' + 'relating ' + 'element axis direction ratios are ' + 'missing', error) + self.apply_validation_function( + self._check_refdirection_dir_ratios(bound), + 'RefDirectionDirectionRatios - ' + 'The space boundary surface on relating element position ' + 'reference direction is missing', error) + return error + + def validate_elements(self, inst: ifcos.entity_instance) -> list: + """Validate elements. + + Validation function for an instance that compiles all instance + validation functions. + Args: + inst:IFC instance being checked + + Returns: + error: list of elements error + + """ + error = [] + self.apply_validation_function(self._check_inst_sb(inst), + 'SpaceBoundaries - ' + 'The instance space boundaries are ' + 'missing', error) + self.apply_validation_function(self._check_inst_materials(inst), + 'MaterialLayers - ' + 'The instance materials are missing', + error) + self.apply_validation_function(self._check_inst_properties(inst), + 'Missing Property_Sets - ' + 'One or more instance\'s necessary ' + 'property sets are missing', error) + self.apply_validation_function( + self._check_inst_contained_in_structure(inst), + 'ContainedInStructure - ' + 'The instance is not contained in any ' + 'structure', error) + self.apply_validation_function(self._check_inst_representation(inst), + 'Representation - ' + 'The instance has no geometric ' + 'representation', error) + return error + + +class CheckLogicHVAC(CheckLogicBase): + """Provides additional logic for ifc files checking regarding HVAC.""" + + @staticmethod + def _check_assignments(inst: ifcos.entity_instance) -> bool: + """Check that the inst (also spec. port) has at least one assignment. + + Args: + port: port ifc entity + + Returns: + True: if check succeeds + False: if check fails + """ + return any(assign.is_a('IfcRelAssignsToGroup') for assign in + inst.HasAssignments) + + @staticmethod + def _check_connection(port: ifcos.entity_instance) -> bool: + """Check that the port is: "connected_to" or "connected_from". + + Args: + port: port ifc entity + + Returns: + True: if check succeeds + False: if check fails + """ + return len(port.ConnectedTo) > 0 or len(port.ConnectedFrom) > 0 + + @staticmethod + def _check_contained_in(port: ifcos.entity_instance) -> bool: + """ + Check that the port is "contained_in". + + Args: + port: port ifc entity + + Returns: + True: if check succeeds + False: if check fails + """ + return len(port.ContainedIn) > 0 + + # elements check + @staticmethod + def _check_inst_ports(inst: ifcos.entity_instance) -> bool: + """Check that an instance has associated ports. + + Args: + inst: IFC instance + + Returns: + True: if check succeeds + False: if check fails + """ + ports = get_ports(inst) + if ports: + return True + else: + return False + + @staticmethod + def _check_contained_in_structure(inst: ifcos.entity_instance) -> bool: + """Check that an instance is contained in an structure. + + Args: + inst: IFC instance + + Returns: + True: if check succeeds + False: if check fails + """ + if hasattr(inst, 'ContainedInStructure'): + return len(inst.ContainedInStructure) > 0 + else: + return False + + def _check_inst_properties(self, inst: ifcos.entity_instance): + """Check necessaries property sets and properties. + + Check that an instance has the property sets and properties + necessaries to the plugin. + Args: + inst: IFC instance + + Returns: + True: if check succeeds + False: if check fails + """ + inst_prop2check = self.ps_summary.get(inst.is_a(), {}) + inst_prop = get_property_sets(inst, self.ifc_units) + inst_prop_errors = [] + for prop2check, ps2check in inst_prop2check.items(): + ps = inst_prop.get(ps2check[0], None) + if ps: + if not ps.get(ps2check[1], None): + inst_prop_errors.append( + prop2check+' - '+', '.join(ps2check)) + else: + inst_prop_errors.append(prop2check+' - '+', '.join(ps2check)) + if inst_prop_errors: + key = inst.GlobalId + ' ' + inst.is_a() + self.error_summary_prop.update({key: inst_prop_errors}) + return False + return True + + @staticmethod + def _check_inst_representation(inst: ifcos.entity_instance): + """Check that an instance has a correct geometric representation. + + Args: + inst: IFC instance + + Returns: + True: if check succeeds + False: if check fails + """ + if hasattr(inst, 'Representation'): + return inst.Representation is not None + else: + return False + + def validate_sub_inst(self, port: ifcos.entity_instance) -> list: + """Run validation functions for a port. + + Args: + port: IFC port entity + + Returns: + error: list of errors found in the IFC port + + """ + error = [] + self.apply_validation_function(self._check_assignments(port), + 'Assignments - ' + 'The port assignments are missing', + error) + self.apply_validation_function(self._check_connection(port), + 'Connections - ' + 'The port has no connections', error) + self.apply_validation_function(self._check_contained_in(port), + 'ContainedIn - ' + 'The port is not contained in', error) + return error + + def validate_elements(self, inst: ifcos.entity_instance) -> list: + """Validate elements (carrier function). + + Validation function for an instance that compiles all instance + validation functions. + + Args: + inst: IFC instance being checked + + Returns: + error: list of elements error + + """ + error = [] + + self.apply_validation_function(self._check_inst_ports(inst), + 'Ports - ' + 'The instance ports are missing', error) + self.apply_validation_function( + self._check_contained_in_structure(inst), + 'ContainedInStructure - ' + 'The instance is not contained in any ' + 'structure', error) + self.apply_validation_function(self._check_inst_properties(inst), + 'Missing Property_Sets - ' + 'One or more instance\'s necessary ' + 'property sets are missing', error) + self.apply_validation_function(self._check_inst_representation(inst), + 'Representation - ' + 'The instance has no geometric ' + 'representation', error) + self.apply_validation_function(self._check_assignments(inst), + 'Assignments - ' + 'The instance assignments are missing', + error) + return error + + +if __name__ == '__main__': + pass diff --git a/bim2sim/tasks/common/check_ifc.py b/bim2sim/tasks/checks/check_ifc_old.py similarity index 100% rename from bim2sim/tasks/common/check_ifc.py rename to bim2sim/tasks/checks/check_ifc_old.py diff --git a/bim2sim/tasks/common/__init__.py b/bim2sim/tasks/common/__init__.py index 2aceb2c45d..8c8f17e47b 100644 --- a/bim2sim/tasks/common/__init__.py +++ b/bim2sim/tasks/common/__init__.py @@ -1,7 +1,6 @@ from .create_relations import CreateRelations from .base_tasks import Reset, Quit from .load_ifc import LoadIFC -from .check_ifc import CheckIfc, CheckIfcBPS, CheckIfcHVAC from .create_elements import CreateElementsOnIfcTypes from .weather import Weather from .serialize_elements import SerializeElements diff --git a/bim2sim/tasks/common/load_ifc.py b/bim2sim/tasks/common/load_ifc.py index d401658b6a..4f9ecf3458 100644 --- a/bim2sim/tasks/common/load_ifc.py +++ b/bim2sim/tasks/common/load_ifc.py @@ -8,6 +8,26 @@ from bim2sim.utilities.types import IFCDomain +@staticmethod +def extract_ifc_file_names(base_path): + """Extract ifc file names of a given directory. + + Args: + base_path: Pathlib path that holds the different domain folders, + which hold the ifc files. + Returns: + list: of ifc file names + """ + if not base_path.is_dir(): + raise AssertionError(f"Given base_path {base_path} is not a" + f" directory. Please provide a directory.") + ifc_file_paths = list(base_path.glob("**/*.ifc")) + list( + base_path.glob("**/*.ifcxml")) + list( + base_path.glob("**/*.ifczip")) + + return ifc_file_paths + + class LoadIFC(ITask): """Load all IFC files from PROJECT.ifc_base path. @@ -16,15 +36,17 @@ class LoadIFC(ITask): Returns: ifc: list of one or multiple IfcFileClass elements """ + touches = ('ifc_files', ) def run(self): + """Run function of LoadIFC.""" self.logger.info("Loading IFC files") ifc_files = yield from self.load_ifc_files(self.paths.ifc_base) return ifc_files, def load_ifc_files(self, base_path: Path): - """Load all ifc files in given base_path or a specific file in this path + """Load all ifc files/file given in base_path. Loads the ifc files inside the different domain folders in the base path, and initializes the bim2sim ifc file classes. @@ -33,20 +55,16 @@ def load_ifc_files(self, base_path: Path): base_path: Pathlib path that holds the different domain folders, which hold the ifc files. """ - if not base_path.is_dir(): - raise AssertionError(f"Given base_path {base_path} is not a" - f" directory. Please provide a directory.") + ifc_files_paths = extract_ifc_file_names(base_path) + self.logger.info(f"Found {len(ifc_files_paths)} IFC files in project " + f"directory.") ifc_files_dict = {k: [] for k in ['arch', 'hydraulic', 'ventilation']} ifc_files_unsorted = [] ifc_files = [] - ifc_files_paths = list(base_path.glob("**/*.ifc")) + list( - base_path.glob("**/*.ifcxml")) + list( - base_path.glob("**/*.ifczip")) - self.logger.info(f"Found {len(ifc_files_paths)} IFC files in project " - f"directory.") for i, total_ifc_path in enumerate(ifc_files_paths, start=1): self.logger.info( - f"Loading IFC file {total_ifc_path.name} {i}/{len(ifc_files_paths)}.") + f"Loading IFC file {total_ifc_path.name} {i}/" + "{len(ifc_files_paths)}.") ifc_domain = total_ifc_path.parent.name reset_guids = self.playground.sim_settings.reset_guids ifc_domain = IFCDomain[ifc_domain] @@ -70,8 +88,9 @@ def load_ifc_files(self, base_path: Path): self.logger.error("No ifc found in project folder.") raise AssertionError("No ifc found. Check '%s'" % base_path) elif len(ifc_files) < len(ifc_files_unsorted): - self.logger.warning("Not all ifc files were added for further " - "processing. IFCDomain may not be recognized.") + self.logger.warning( + "Not all ifc files were added for further " + "processing. IFCDomain may not be recognized.") raise AssertionError("Not all ifc processed. Check '%s'" % base_path) self.logger.info(f"Loaded {len(ifc_files)} IFC-files.") diff --git a/pyproject.toml b/pyproject.toml index 6880ce563c..5a0bc7d6b2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -125,6 +125,10 @@ PluginOpenFOAM = [ "seaborn", "lbt-butterfly@git+https://github.com/BIM2SIM/butterfly" ] +PluginIFCCheck = [ + "ifctester==0.8.1", + "pystache==0.6.8" # is a dependency from ifctester, not automatically install here +] docu = [ "sphinx==6.2.1", "myst-parser", diff --git a/test/resources b/test/resources index a30bdad1c6..e5355ab6f2 160000 --- a/test/resources +++ b/test/resources @@ -1 +1 @@ -Subproject commit a30bdad1c6103d18c2a504dad1835ad2f5d6aeaf +Subproject commit e5355ab6f291d69c31e13756d0748a8018b06084 diff --git a/test/unit/tasks/common/test_load_ifc.py b/test/unit/tasks/common/test_load_ifc.py index 4cfe71e93d..0b27244e0c 100644 --- a/test/unit/tasks/common/test_load_ifc.py +++ b/test/unit/tasks/common/test_load_ifc.py @@ -2,33 +2,36 @@ import tempfile import unittest from pathlib import Path -from unittest import mock import bim2sim from bim2sim.kernel.decision.decisionhandler import DebugDecisionHandler from bim2sim.tasks.common import LoadIFC +from test.unit.tasks import TestTask +from test.unit.elements.helper import SetupHelper +from bim2sim.sim_settings import BaseSimSettings + test_rsrc_path = Path(__file__).parent.parent.parent.parent / 'resources' -class TestLoadIFC(unittest.TestCase): - load_task = None - export_path = None - playground = None +class TestLoadIFC(TestTask): + @classmethod + def simSettingsClass(cls): + return BaseSimSettings() + + @classmethod + def testTask(cls): + return LoadIFC(cls.playground) + + @classmethod + def helper(cls): + return SetupHelper() + # define setUpClass @classmethod def setUpClass(cls) -> None: - # Set up playground, project and paths via mocks - cls.playground = mock.Mock() - project = mock.Mock() - paths = mock.Mock() - cls.playground.project = project - - # Instantiate export task and set required values via mocks - cls.load_ifc_task = LoadIFC(cls.playground) - cls.load_ifc_task.prj_name = 'TestLoadIFC' - cls.load_ifc_task.paths = paths - cls.load_ifc_task.paths.finder = ( + super().setUpClass() + cls.test_task.paths.finder = ( Path(bim2sim.__file__).parent / 'assets/finder/template_ArchiCAD.json') @@ -42,8 +45,8 @@ def test_load_ifc(self): destination_file = subdir_path / source_file.name shutil.copy2(source_file, destination_file) - self.load_ifc_task.paths.ifc_base = temp_path / 'ifc' - touches = DebugDecisionHandler(answers=()).handle(self.load_ifc_task.run()) + self.test_task.paths.ifc_base = temp_path / 'ifc' + touches = DebugDecisionHandler(answers=()).handle(self.test_task.run()) ifc_file_name = touches[0][0].ifc_file_name ifc_schema = touches[0][0].schema @@ -62,9 +65,9 @@ def test_load_ifczip(self): destination_file = subdir_path / source_file.name shutil.copy2(source_file, destination_file) - self.load_ifc_task.paths.ifc_base = temp_path / 'ifc' + self.test_task.paths.ifc_base = temp_path / 'ifc' touches = DebugDecisionHandler( - answers=()).handle(self.load_ifc_task.run()) + answers=()).handle(self.test_task.run()) ifc_file_name = touches[0][0].ifc_file_name ifc_schema = touches[0][0].schema @@ -85,9 +88,9 @@ def test_load_ifcxml(self): destination_file = subdir_path / source_file.name shutil.copy2(source_file, destination_file) - self.load_ifc_task.paths.ifc_base = temp_path / 'ifc' + self.test_task.paths.ifc_base = temp_path / 'ifc' touches = DebugDecisionHandler( - answers=()).handle(self.load_ifc_task.run()) + answers=()).handle(self.test_task.run()) ifc_file_name = touches[0][0].ifc_file_name ifc_schema = touches[0][0].schema