Skip to content

Commit 227f315

Browse files
committed
Support quantity take-off for element types
Previously it would ignore any element types and work only on occurrences though quantity sets do support product types in general.
1 parent d55a291 commit 227f315

1 file changed

Lines changed: 91 additions & 16 deletions

File tree

src/ifc5d/ifc5d/qto.py

Lines changed: 91 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,10 @@
2828
import ifcopenshell.util.selector
2929
import ifcopenshell.util.shape
3030
import ifcopenshell.util.representation
31+
import ifcopenshell.util.type
3132
import multiprocessing
32-
from collections import namedtuple
33-
from typing import Any, Literal, get_args, Union
33+
from collections import namedtuple, defaultdict
34+
from typing import Any, Literal, get_args, Union, Iterable
3435

3536

3637
Function = namedtuple("Function", ["measure", "name", "description"])
@@ -52,10 +53,19 @@ def quantify(ifc_file: ifcopenshell.file, elements: set[ifcopenshell.entity_inst
5253
5354
"""
5455
results: ResultsDict = {}
56+
elements_by_classes: defaultdict[str, set[ifcopenshell.entity_instance]] = defaultdict(set)
57+
for element in elements:
58+
elements_by_classes[element.is_a()].add(element)
59+
5560
for calculator, queries in rules["calculators"].items():
5661
calculator = calculators[calculator]
57-
for query, qtos in queries.items():
58-
filtered_elements = ifcopenshell.util.selector.filter_elements(ifc_file, query, elements)
62+
for ifc_class, qtos in queries.items():
63+
filtered_elements = set()
64+
ifc_classes = [ifc_class] + ifcopenshell.util.type.get_applicable_types(ifc_class)
65+
for ifc_class in ifc_classes:
66+
if ifc_class not in elements_by_classes:
67+
continue
68+
filtered_elements.update(elements_by_classes[ifc_class])
5969
if filtered_elements:
6070
calculator.calculate(ifc_file, filtered_elements, qtos, results)
6171
return results
@@ -100,6 +110,55 @@ def convert(self, value: float, measure: str) -> float:
100110
return value
101111

102112

113+
class IteratorForTypes:
114+
"""Currently ifcopenshell.geom.iterator support only IfcProducts, so this
115+
class is mimicking the iterator interface but works for IfcTypeProducts."""
116+
117+
element: Union[ifcopenshell.entity_instance, None] = None
118+
shape: Union[ifcopenshell.geom.ShapeType, None] = None
119+
120+
def __init__(
121+
self,
122+
ifc_file: ifcopenshell.file,
123+
settings: ifcopenshell.geom.settings,
124+
elements: Iterable[ifcopenshell.entity_instance],
125+
):
126+
self.settings = settings
127+
self.elements = list(elements)
128+
self.element = None
129+
self.file = ifc_file
130+
model = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW")
131+
assert model
132+
self.context = model
133+
134+
def initialize(self) -> bool:
135+
return bool(self.next())
136+
137+
def get_element_and_geometry(self) -> tuple[ifcopenshell.entity_instance, ifcopenshell.geom.ShapeType]:
138+
# get() is not implemented so it won't be confused with iteartor.get().
139+
# The difference is important since create_shape for product types
140+
# doesn't ouput SpapeElementType, only ShapeTypes.
141+
assert self.element and self.shape
142+
return (self.element, self.shape)
143+
144+
def next(self) -> bool:
145+
if not self.elements:
146+
return False
147+
while self.elements:
148+
element = self.elements.pop()
149+
if self.process_shape(element):
150+
return True
151+
return False
152+
153+
def process_shape(self, element: ifcopenshell.entity_instance):
154+
representation = ifcopenshell.util.representation.get_representation(element, self.context)
155+
if not representation:
156+
return False
157+
self.shape = ifcopenshell.geom.create_shape(self.settings, representation)
158+
self.element = element
159+
return True
160+
161+
103162
class IfcOpenShell:
104163
"""Calculates Model body context geometry using the default IfcOpenShell
105164
iterator on triangulation elements."""
@@ -189,44 +248,60 @@ def calculate(
189248
gross_or_net_qtos.setdefault(name, {})[quantity] = formula
190249
formula_functions[formula] = getattr(ifcopenshell.util.shape, formula)
191250

192-
tasks: list[tuple[ifcopenshell.geom.iterator, QtosFormulas]] = []
251+
tasks: list[tuple[Union[ifcopenshell.geom.iterator, IteratorForTypes], QtosFormulas]] = []
193252

194253
if gross_qtos:
195-
tasks.append((IfcOpenShell.create_iterator(ifc_file, cls.gross_settings, list(elements)), gross_qtos))
254+
for iterator in IfcOpenShell.create_iterators(ifc_file, cls.gross_settings, list(elements)):
255+
tasks.append((iterator, gross_qtos))
196256

197257
if net_qtos:
198-
tasks.append((IfcOpenShell.create_iterator(ifc_file, cls.net_settings, list(elements)), net_qtos))
258+
for iterator in IfcOpenShell.create_iterators(ifc_file, cls.gross_settings, list(elements)):
259+
tasks.append((iterator, net_qtos))
199260

200261
cls.unit_converter = SI2ProjectUnitConverter(ifc_file)
201262

202263
for iterator, qtos_ in tasks:
203264
if iterator.initialize():
204265
while True:
205-
shape = iterator.get()
206-
element = ifc_file.by_id(shape.id)
266+
if isinstance(iterator, ifcopenshell.geom.iterator):
267+
shape = iterator.get()
268+
geometry = shape.geometry
269+
element = ifc_file.by_id(shape.id)
270+
else:
271+
element, geometry = iterator.get_element_and_geometry()
272+
207273
results.setdefault(element, {})
208274
for name, quantities in qtos_.items():
209275
results[element].setdefault(name, {})
210276
for quantity, formula in quantities.items():
211277
if formula == "get_segment_length":
212-
results[element][name][quantity] = cls.get_segment_length(ifc_file, shape)
278+
results[element][name][quantity] = cls.get_segment_length(element)
213279
else:
214280
results[element][name][quantity] = cls.unit_converter.convert(
215-
formula_functions[formula](shape.geometry),
281+
formula_functions[formula](geometry),
216282
IfcOpenShell.raw_functions[formula].measure,
217283
)
218284
if not iterator.next():
219285
break
220286

221287
@staticmethod
222-
def create_iterator(
288+
def create_iterators(
223289
ifc_file: ifcopenshell.file, settings: ifcopenshell.geom.settings, elements: list[ifcopenshell.entity_instance]
224-
) -> ifcopenshell.geom.iterator:
225-
return ifcopenshell.geom.iterator(settings, ifc_file, multiprocessing.cpu_count(), include=elements)
290+
) -> list[Union[ifcopenshell.geom.iterator, IteratorForTypes]]:
291+
elements_sorted: defaultdict[bool, list[ifcopenshell.entity_instance]] = defaultdict(list)
292+
iterators = []
293+
for element in elements:
294+
elements_sorted[element.is_a("IfcTypeProduct")].append(element)
295+
if True in elements_sorted:
296+
iterators.append(IteratorForTypes(ifc_file, settings, elements_sorted[True]))
297+
if False in elements_sorted:
298+
iterators.append(
299+
ifcopenshell.geom.iterator(settings, ifc_file, multiprocessing.cpu_count(), include=elements)
300+
)
301+
return iterators
226302

227303
@classmethod
228-
def get_segment_length(cls, ifc_file: ifcopenshell.file, shape) -> float:
229-
element = ifc_file.by_id(shape.id)
304+
def get_segment_length(cls, element: ifcopenshell.entity_instance) -> float:
230305
rep = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
231306
if rep and len(rep.Items or []) == 1 and rep.Items[0].is_a("IfcExtrudedAreaSolid"):
232307
item = rep.Items[0]

0 commit comments

Comments
 (0)