mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-24 21:59:59 +00:00
Add assign constituent fractions recipe (#5979)
* Add AssignConstituentFractions recipe * license, units & added context description * removed src, using better unit handling and several other small improvements based on feedback
This commit is contained in:
@@ -0,0 +1,149 @@
|
|||||||
|
# IfcPatch - IFC patching utility
|
||||||
|
# Copyright (C) 2024 Louis Trümpler <louis@lt.plus>
|
||||||
|
#
|
||||||
|
# This file is part of IfcPatch.
|
||||||
|
#
|
||||||
|
# IfcPatch 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.
|
||||||
|
#
|
||||||
|
# IfcPatch 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 IfcPatch. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import ifcopenshell
|
||||||
|
import ifcopenshell.util.element
|
||||||
|
import ifcopenshell.util.unit
|
||||||
|
from logging import Logger
|
||||||
|
from collections import defaultdict
|
||||||
|
from typing import List, Dict, Tuple, Optional
|
||||||
|
|
||||||
|
class Patcher:
|
||||||
|
"""
|
||||||
|
Assigns fractions to material constituents based on their "width".
|
||||||
|
|
||||||
|
In Reference View MVD, material layer information is exchanged through
|
||||||
|
IfcMaterialConstituentSet and IfcPhysicalComplexQuantity instead of IfcMaterialLayerSet.
|
||||||
|
The layer thicknesses are stored as IfcQuantityLength within IfcPhysicalComplexQuantity
|
||||||
|
with a Discrimination of 'layer'.
|
||||||
|
|
||||||
|
While the Fraction attribute in IfcMaterialConstituent is optional, it's valuable for
|
||||||
|
downstream applications as it indicates the relative proportion of each constituent in
|
||||||
|
the total material composition in a more straightforward way.
|
||||||
|
However, authoring applications often export material constituents without setting this attribute.
|
||||||
|
|
||||||
|
This patcher helps maintain proper material information in Reference View exports by:
|
||||||
|
1. Finding width quantities from IfcPhysicalComplexQuantity with 'layer' discrimination
|
||||||
|
2. Using these widths to calculate relative proportions
|
||||||
|
3. Setting the optional but valuable Fraction attribute
|
||||||
|
|
||||||
|
For example, if a wall has constituents with widths of 0.1m and 0.2m, their fractions
|
||||||
|
would be set to 0.333 and 0.667 respectively.
|
||||||
|
|
||||||
|
References:
|
||||||
|
- IFC4 Reference View MVD: https://standards.buildingsmart.org/MVD/RELEASE/IFC4/ADD2_TC1/RV1_2/HTML/
|
||||||
|
- IfcMaterialConstituent: https://standards.buildingsmart.org/IFC/RELEASE/IFC4/ADD2_TC1/HTML/schema/ifcmaterialresource/lexical/ifcmaterialconstituent.htm
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, file: ifcopenshell.file, logger: Logger, *args):
|
||||||
|
self.file = file
|
||||||
|
self.logger = logger
|
||||||
|
|
||||||
|
def patch(self):
|
||||||
|
"""Execute the patch to assign fractions to material constituents."""
|
||||||
|
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(self.file)
|
||||||
|
|
||||||
|
# Get length unit from project
|
||||||
|
length_unit = ifcopenshell.util.unit.get_project_unit(self.file, "LENGTHUNIT")
|
||||||
|
|
||||||
|
for constituent_set in self.file.by_type('IfcMaterialConstituentSet'):
|
||||||
|
if not (constituents := constituent_set.MaterialConstituents):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Find elements associated with this constituent set
|
||||||
|
if not (elements := set(ifcopenshell.util.element.get_elements_by_material(self.file, constituent_set))):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Sort elements by GlobalId to ensure consistent order
|
||||||
|
elements_sorted = sorted(elements, key=lambda x: x.GlobalId)
|
||||||
|
for element in elements_sorted:
|
||||||
|
quantities = self.get_element_quantities(element)
|
||||||
|
if quantities:
|
||||||
|
element_quantities = quantities
|
||||||
|
break
|
||||||
|
|
||||||
|
if not element_quantities:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Calculate constituent widths and total width
|
||||||
|
constituent_widths, total_width = self.calculate_constituent_widths(
|
||||||
|
constituents, elements, unit_scale
|
||||||
|
)
|
||||||
|
|
||||||
|
if not constituent_widths:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Assign fractions based on widths
|
||||||
|
for constituent, width in constituent_widths.items():
|
||||||
|
fraction = width / total_width
|
||||||
|
constituent.Fraction = fraction
|
||||||
|
self.logger.info(
|
||||||
|
f"Constituent: {constituent.Name}, "
|
||||||
|
f"Width: {width:.4f} {length_unit}, "
|
||||||
|
f"Fraction: {fraction:.4f}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_element_quantities(self, element: ifcopenshell.entity_instance) -> Dict[str, float]:
|
||||||
|
"""Get width quantities for an element."""
|
||||||
|
qtos = [v for k,v in ifcopenshell.util.element.get_psets(element, qtos_only=True).items()
|
||||||
|
if k.endswith("BaseQuantities")]
|
||||||
|
if qtos:
|
||||||
|
return {k:v["properties"]["Width"] for k,v in qtos[0].items()
|
||||||
|
if isinstance(v,dict) and
|
||||||
|
(v.get("Discrimination") or "").lower() == "layer" and
|
||||||
|
v.get("properties",{}).get("Width", "") is not None}
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def calculate_constituent_widths(
|
||||||
|
self,
|
||||||
|
constituents: List[ifcopenshell.entity_instance],
|
||||||
|
elements: set[ifcopenshell.entity_instance],
|
||||||
|
unit_scale: float
|
||||||
|
) -> Tuple[Dict[ifcopenshell.entity_instance, float], float]:
|
||||||
|
"""Calculate the widths of constituents based on associated quantities."""
|
||||||
|
if not elements:
|
||||||
|
return {}, 0.0
|
||||||
|
|
||||||
|
# Get quantities from all elements and use the first valid one
|
||||||
|
element_quantities = {}
|
||||||
|
for element in elements:
|
||||||
|
quantities = self.get_element_quantities(element)
|
||||||
|
if quantities: # If we found valid quantities, use them
|
||||||
|
element_quantities = quantities
|
||||||
|
self.logger.debug(f"Using quantities from element: {element.GlobalId}")
|
||||||
|
break
|
||||||
|
|
||||||
|
if not element_quantities:
|
||||||
|
self.logger.warning("No valid quantities found in any element")
|
||||||
|
return {}, 0.0
|
||||||
|
|
||||||
|
constituent_widths = {}
|
||||||
|
total_width = 0.0
|
||||||
|
|
||||||
|
for constituent in constituents:
|
||||||
|
if not constituent.Name: # Skip unnamed constituents as per RV MVD
|
||||||
|
continue
|
||||||
|
|
||||||
|
constituent_name = constituent.Name.strip()
|
||||||
|
if width := element_quantities.get(constituent_name):
|
||||||
|
constituent_widths[constituent] = width * unit_scale
|
||||||
|
total_width += width * unit_scale
|
||||||
|
else:
|
||||||
|
self.logger.debug(f"No width found for constituent: {constituent_name}")
|
||||||
|
|
||||||
|
return constituent_widths, total_width
|
||||||
Reference in New Issue
Block a user