From 5159e2daee3d12003de9a5fffcc3ba5f1c2d08b8 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Tue, 28 May 2024 17:32:51 +1000 Subject: [PATCH] Initial attempt at a qto module in Ifc5D This is a more flexible, non-Blender approach to doing QTO which allows: - Doing QTO without Blender - Doing QTO using query filters - Using config files for QTOs - Choosing your calculation engine, either built-in or creating your own --- src/ifc5d/ifc5d/IFC4QtoBaseQuantities.json | 17 +++ src/ifc5d/ifc5d/qto.py | 135 +++++++++++++++++++++ 2 files changed, 152 insertions(+) create mode 100644 src/ifc5d/ifc5d/IFC4QtoBaseQuantities.json create mode 100644 src/ifc5d/ifc5d/qto.py diff --git a/src/ifc5d/ifc5d/IFC4QtoBaseQuantities.json b/src/ifc5d/ifc5d/IFC4QtoBaseQuantities.json new file mode 100644 index 0000000000..f0627c19da --- /dev/null +++ b/src/ifc5d/ifc5d/IFC4QtoBaseQuantities.json @@ -0,0 +1,17 @@ +{ + "name": "IFC4 Base Quantities", + "description": "This ruleset quantifies every single possible standardised base quantity in IFC4", + "calculators": { + "IOSTriangulation": { + "IfcWall": { + "Qto_WallBaseQuantities": { + "Length": "net_get_x", + "Width": "net_get_y", + "Height": "net_get_z", + "NetSideArea": "net_get_side_area", + "NetVolume": "net_get_volume" + } + } + } + } +} diff --git a/src/ifc5d/ifc5d/qto.py b/src/ifc5d/ifc5d/qto.py new file mode 100644 index 0000000000..ff322073a8 --- /dev/null +++ b/src/ifc5d/ifc5d/qto.py @@ -0,0 +1,135 @@ +# Ifc5D - IFC costing utility +# Copyright (C) 2021 Dion Moult +# +# This file is part of Ifc5D. +# +# Ifc5D is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ifc5D is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Ifc5D. If not, see . + +import os +import json +import ifcopenshell +import ifcopenshell.api +import ifcopenshell.api.pset +import ifcopenshell.util.selector +import multiprocessing +from typing import Optional + + +def get_rules(name: str): + cwd = os.path.dirname(os.path.realpath(__file__)) + with open(os.path.join(cwd, name + ".json"), "r") as f: + return json.load(f) + + +def quantify(ifc_file: ifcopenshell.file, elements: set[ifcopenshell.entity_instance], rules: dict): + results = {} + for calculator, queries in rules["calculators"].items(): + calculator = calculators[calculator] + for query, qtos in queries.items(): + filtered_elements = ifcopenshell.util.selector.filter_elements(ifc_file, query, elements) + calculator.calculate(ifc_file, filtered_elements, qtos, results) + return results + + +def edit_qtos(ifc_file, results): + for element, qtos in results.items(): + for name, quantities in qtos.items(): + qto = ifcopenshell.util.element.get_pset(element, name, should_inherit=False) + if qto: + qto = ifc_file.by_id(qto["id"]) + else: + qto = ifcopenshell.api.pset.add_qto(ifc_file, element, name) + ifcopenshell.api.pset.edit_qto(ifc_file, qto=qto, properties=quantities) + + +class IOSTriangulation: + @staticmethod + def calculate( + ifc_file: ifcopenshell.file, + elements: set[ifcopenshell.entity_instance], + qtos: dict, + results: Optional[dict] = None, + ): + import ifcopenshell + import ifcopenshell.geom + import ifcopenshell.util.shape + + if results is None: + results = {} + + formula_functions = {} + + gross_settings = ifcopenshell.geom.settings() + gross_settings.set(gross_settings.DISABLE_OPENING_SUBTRACTIONS, True) + net_settings = ifcopenshell.geom.settings() + + gross_qtos = {} + net_qtos = {} + + for name, quantities in qtos.items(): + for quantity, formula in quantities.items(): + if formula.startswith("gross_"): + formula = formula[6:] + gross_qtos.setdefault(name, {})[quantity] = formula + formula_functions[formula] = getattr(ifcopenshell.util.shape, formula) + elif formula.startswith("net_"): + formula = formula[4:] + net_qtos.setdefault(name, {})[quantity] = formula + formula_functions[formula] = getattr(ifcopenshell.util.shape, formula) + + tasks = [] + + if gross_qtos: + tasks.append((IOSTriangulation.create_iterator(ifc_file, gross_settings, elements), gross_qtos)) + + if net_qtos: + tasks.append((IOSTriangulation.create_iterator(ifc_file, net_settings, elements), net_qtos)) + + for iterator, qtos in tasks: + if iterator.initialize(): + while True: + shape = iterator.get() + element = ifc_file.by_id(shape.id) + results.setdefault(element, {}) + for name, quantities in qtos.items(): + results[element].setdefault(name, {}) + for quantity, formula in quantities.items(): + results[element][name][quantity] = formula_functions[formula](shape.geometry) + if not iterator.next(): + break + + return results + + @staticmethod + def create_iterator(ifc_file, settings, elements): + return ifcopenshell.geom.iterator(settings, ifc_file, multiprocessing.cpu_count(), include=elements) + + +class Blender: + @staticmethod + def calculate(ifc_file, elements, qtos): + import blenderbim.tool as tool + + for element in elements: + obj = tool.Ifc.get_object(element) + if not obj: + continue + + for name, quantities in qtos.items(): + for quantity, formula in quantities.items(): + getattr(tool.Qto, formula) + # TODO + + +calculators = {"Blender": Blender, "IOSTriangulation": IOSTriangulation}