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
+
+
+
+ | GUID |
+ Name |
+
+
+ %for guid, values in double_guids.items():
+
+ | ${guid} |
+ ${values.Name} |
+
+ %endfor
+
+
+
+Empty GUIDs
+
+
+
+ | Generated Key (bim2sim) |
+ Name |
+ GlobalID |
+
+
+ %for key, values in empty_guids.items():
+
+ | ${key} |
+ ${values.Name} |
+ ${values.GlobalId} |
+
+ %endfor
+
+
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