Coverage for bim2sim/elements/aggregation/bps_aggregations.py: 42%
219 statements
« prev ^ index » next coverage.py v7.6.12, created at 2025-03-12 17:09 +0000
« prev ^ index » next coverage.py v7.6.12, created at 2025-03-12 17:09 +0000
1import logging
2from typing import Set, TYPE_CHECKING
3from ifcopenshell import guid
5from bim2sim.elements import bps_elements as bps
6from bim2sim.elements.aggregation import AggregationMixin
7from bim2sim.elements.bps_elements import InnerFloor, Roof, OuterWall, \
8 GroundFloor, InnerWall, Window, InnerDoor, OuterDoor, Slab, Wall, Door, \
9 ExtSpatialSpaceBoundary
10from bim2sim.elements.mapping import attribute
11from bim2sim.elements.mapping.units import ureg
12from bim2sim.utilities.common_functions import filter_elements
13from bim2sim.utilities.types import AttributeDataSource
15if TYPE_CHECKING:
16 from bim2sim.elements.bps_elements import (BPSProduct, SpaceBoundary,
17 ThermalZone)
18logger = logging.getLogger(__name__)
21class AggregatedThermalZone(AggregationMixin, bps.ThermalZone):
22 """Aggregates thermal zones"""
23 aggregatable_elements = {bps.ThermalZone}
25 def __init__(self, elements, *args, **kwargs):
26 super().__init__(elements, *args, **kwargs)
27 # self.get_disaggregation_properties()
28 self.bound_elements = self.bind_elements()
29 self.bind_tz_to_storeys()
30 self.bind_tz_to_building()
31 self.description = ''
32 # todo lump usage conditions of existing zones
34 def bind_elements(self):
35 """elements binder for the resultant thermal zone"""
36 bound_elements = []
37 for tz in self.elements:
38 for inst in tz.bound_elements:
39 if inst not in bound_elements:
40 bound_elements.append(inst)
41 return bound_elements
43 def bind_tz_to_storeys(self):
44 storeys = []
45 for tz in self.elements:
46 for storey in tz.storeys:
47 if storey not in storeys:
48 storeys.append(storey)
49 if self not in storey.thermal_zones:
50 storey.thermal_zones.append(self)
51 if tz in storey.thermal_zones:
52 storey.thermal_zones.remove(tz)
53 self.storeys = storeys
55 def bind_tz_to_building(self):
56 # there should be only one building, but to be sure use list and check
57 buildings = []
58 for tz in self.elements:
59 building = tz.building
60 if building not in buildings:
61 buildings.append(building)
62 if self not in building.thermal_zones:
63 building.thermal_zones.append(self)
64 if tz in building.thermal_zones:
65 building.thermal_zones.remove(tz)
67 if len(buildings) > 1:
68 raise ValueError(
69 f"An AggregatedThermalZone should only contain ThermalZone "
70 f"elements from the same Building. But {self} contains "
71 f"ThermalZone elements from {buildings}.")
72 else:
73 tz.building = buildings[0]
75 @classmethod
76 def find_matches(cls, groups, elements):
77 """creates a new thermal zone aggregation instance
78 based on a previous filtering"""
79 new_aggregations = []
80 thermal_zones = filter_elements(elements, 'ThermalZone')
81 total_area = sum(i.gross_area for i in thermal_zones)
82 for group, group_elements in groups.items():
83 aggregated_tz = None
84 if group == 'one_zone_building':
85 name = "Aggregated_%s" % group
86 aggregated_tz = cls.create_aggregated_tz(
87 name, group, group_elements, elements)
88 elif group == 'not_combined':
89 # last criterion no similarities
90 area = sum(i.gross_area for i in groups[group])
91 if area / total_area <= 0.05:
92 # Todo: usage and conditions criterion
93 name = "Aggregated_not_neighbors"
94 aggregated_tz = cls.create_aggregated_tz(
95 name, group, group_elements, elements)
96 else:
97 # first criterion based on similarities
98 # todo reuse this if needed but currently it doesn't seem so
99 # group_name = re.sub('[\'\[\]]', '', group)
100 group_name = group
101 name = "Aggregated_%s" % group_name.replace(', ', '_')
102 aggregated_tz = cls.create_aggregated_tz(
103 name, group, group_elements, elements)
104 if aggregated_tz:
105 new_aggregations.append(aggregated_tz)
106 return new_aggregations
108 @classmethod
109 def create_aggregated_tz(cls, name, group, group_elements, elements):
110 aggregated_tz = cls(group_elements)
111 aggregated_tz.name = name
112 aggregated_tz.description = group
113 for tz in aggregated_tz.elements:
114 if tz.guid in elements:
115 del elements[tz.guid]
116 elements[aggregated_tz.guid] = aggregated_tz
117 return aggregated_tz
119 def _calc_net_volume(self, name) -> ureg.Quantity:
120 """Calculate the thermal zone net volume"""
121 return sum(tz.net_volume for tz in self.elements if
122 tz.net_volume is not None)
124 net_volume = attribute.Attribute(
125 functions=[_calc_net_volume],
126 unit=ureg.meter ** 3,
127 dependant_elements='elements'
128 )
130 def _intensive_calc(self, name) -> ureg.Quantity:
131 """intensive properties getter - volumetric mean
132 intensive_attributes = ['t_set_heat', 't_set_cool', 'height',
133 'AreaPerOccupant', 'T_threshold_heating', 'activity_degree_persons',
134 'fixed_heat_flow_rate_persons', 'internal_gains_moisture_no_people',
135 'T_threshold_cooling', 'ratio_conv_rad_persons', 'machines',
136 'ratio_conv_rad_machines', 'lighting_power',
137 'ratio_conv_rad_machines', 'lighting_power', 'fixed_lighting_power',
138 'ratio_conv_rad_lighting', 'maintained_illuminance',
139 'lighting_efficiency_lumen', base_infiltration',
140 'max_user_infiltration', 'min_ahu', 'max_ahu', 'persons']"""
141 prop_sum = sum(
142 getattr(tz, name) * tz.net_volume for tz in self.elements if
143 getattr(tz, name) is not None and tz.net_volume is not None)
144 return prop_sum / self.net_volume
146 def _intensive_list_calc(self, name) -> list:
147 """intensive list properties getter - volumetric mean
148 intensive_list_attributes = ['heating_profile', 'cooling_profile',
149 'persons_profile', 'machines_profile', 'lighting_profile',
150 'max_overheating_infiltration', 'max_summer_infiltration',
151 'winter_reduction_infiltration']"""
152 list_attrs = {'heating_profile': 24, 'cooling_profile': 24,
153 'persons_profile': 24,
154 'machines_profile': 24, 'lighting_profile': 24,
155 'max_overheating_infiltration': 2,
156 'max_summer_infiltration': 3,
157 'winter_reduction_infiltration': 3}
158 length = list_attrs[name]
159 aux = []
160 for x in range(0, length):
161 aux.append(sum(
162 getattr(tz, name)[x] * tz.net_volume for tz in self.elements
163 if getattr(tz, name) is not None and tz.net_volume is not None)
164 / self.net_volume)
165 return aux
167 def _extensive_calc(self, name) -> ureg.Quantity:
168 """extensive properties getter
169 intensive_attributes = ['gross_area', 'net_area', 'volume']"""
170 return sum(getattr(tz, name) for tz in self.elements if
171 getattr(tz, name) is not None)
173 def _bool_calc(self, name) -> bool:
174 """bool properties getter
175 bool_attributes = ['with_cooling', 'with_heating', 'with_ahu',
176 'use_maintained_illuminance']"""
177 # todo: log
178 prop_bool = False
179 for tz in self.elements:
180 prop = getattr(tz, name)
181 if prop is not None:
182 if prop:
183 prop_bool = True
184 break
185 return prop_bool
187 def _get_tz_usage(self, name) -> str:
188 """usage properties getter"""
189 return self.elements[0].usage
191 usage = attribute.Attribute(
192 functions=[_get_tz_usage],
193 )
194 # t_set_heat = attribute.Attribute(
195 # functions=[_intensive_calc],
196 # unit=ureg.degC
197 # )
198 # todo refactor this to remove redundancy for units
199 t_set_heat = bps.ThermalZone.t_set_heat.to_aggregation(_intensive_calc)
201 t_set_cool = attribute.Attribute(
202 functions=[_intensive_calc],
203 unit=ureg.degC,
204 dependant_elements='elements'
205 )
206 t_ground = attribute.Attribute(
207 functions=[_intensive_calc],
208 unit=ureg.degC,
209 dependant_elements='elements'
210 )
211 net_area = attribute.Attribute(
212 functions=[_extensive_calc],
213 unit=ureg.meter ** 2,
214 dependant_elements='elements'
215 )
216 gross_area = attribute.Attribute(
217 functions=[_extensive_calc],
218 unit=ureg.meter ** 2,
219 dependant_elements='elements'
220 )
221 gross_volume = attribute.Attribute(
222 functions=[_extensive_calc],
223 unit=ureg.meter ** 3,
224 dependant_elements='elements'
225 )
226 height = attribute.Attribute(
227 functions=[_intensive_calc],
228 unit=ureg.meter,
229 dependant_elements='elements'
230 )
231 area_per_occupant = attribute.Attribute(
232 functions=[_intensive_calc],
233 unit=ureg.meter ** 2,
234 dependant_elements='elements'
235 )
236 # use conditions
237 with_cooling = attribute.Attribute(
238 functions=[_bool_calc],
239 dependant_elements='elements'
240 )
241 with_heating = attribute.Attribute(
242 functions=[_bool_calc],
243 dependant_elements='elements'
244 )
245 with_ahu = attribute.Attribute(
246 functions=[_bool_calc],
247 dependant_elements='elements'
248 )
249 heating_profile = attribute.Attribute(
250 functions=[_intensive_list_calc],
251 dependant_elements='elements'
252 )
253 cooling_profile = attribute.Attribute(
254 functions=[_intensive_list_calc],
255 dependant_elements='elements'
256 )
257 persons = attribute.Attribute(
258 functions=[_intensive_calc],
259 dependant_elements='elements'
260 )
261 T_threshold_heating = attribute.Attribute(
262 functions=[_intensive_calc],
263 dependant_elements='elements'
264 )
265 activity_degree_persons = attribute.Attribute(
266 functions=[_intensive_calc],
267 dependant_elements='elements'
268 )
269 fixed_heat_flow_rate_persons = attribute.Attribute(
270 functions=[_intensive_calc],
271 dependant_elements='elements'
272 )
273 internal_gains_moisture_no_people = attribute.Attribute(
274 functions=[_intensive_calc],
275 dependant_elements='elements'
276 )
277 T_threshold_cooling = attribute.Attribute(
278 functions=[_intensive_calc],
279 dependant_elements='elements'
280 )
281 ratio_conv_rad_persons = attribute.Attribute(
282 functions=[_intensive_calc],
283 dependant_elements='elements'
284 )
285 machines = attribute.Attribute(
286 functions=[_intensive_calc],
287 dependant_elements='elements'
288 )
289 ratio_conv_rad_machines = attribute.Attribute(
290 functions=[_intensive_calc],
291 dependant_elements='elements'
292 )
293 use_maintained_illuminance = attribute.Attribute(
294 functions=[_bool_calc],
295 dependant_elements='elements'
296 )
297 lighting_power = attribute.Attribute(
298 functions=[_intensive_calc],
299 dependant_elements='elements'
300 )
301 fixed_lighting_power = attribute.Attribute(
302 functions=[_intensive_calc],
303 dependant_elements='elements'
304 )
305 ratio_conv_rad_lighting = attribute.Attribute(
306 functions=[_intensive_calc],
307 dependant_elements='elements'
308 )
309 maintained_illuminance = attribute.Attribute(
310 functions=[_intensive_calc],
311 dependant_elements='elements'
312 )
313 lighting_efficiency_lumen = attribute.Attribute(
314 functions=[_intensive_calc],
315 dependant_elements='elements'
316 )
317 use_constant_infiltration = attribute.Attribute(
318 functions=[_bool_calc],
319 dependant_elements='elements'
320 )
321 base_infiltration = attribute.Attribute(
322 functions=[_intensive_calc],
323 dependant_elements='elements'
324 )
325 max_user_infiltration = attribute.Attribute(
326 functions=[_intensive_calc],
327 dependant_elements='elements'
328 )
329 max_overheating_infiltration = attribute.Attribute(
330 functions=[_intensive_list_calc],
331 dependant_elements='elements'
332 )
333 max_summer_infiltration = attribute.Attribute(
334 functions=[_intensive_list_calc],
335 dependant_elements='elements'
336 )
337 winter_reduction_infiltration = attribute.Attribute(
338 functions=[_intensive_list_calc],
339 dependant_elements='elements'
340 )
341 min_ahu = attribute.Attribute(
342 functions=[_intensive_calc],
343 dependant_elements='elements'
344 )
345 max_ahu = attribute.Attribute(
346 functions=[_intensive_calc],
347 dependant_elements='elements'
348 )
349 with_ideal_thresholds = attribute.Attribute(
350 functions=[_bool_calc],
351 dependant_elements='elements'
352 )
353 persons_profile = attribute.Attribute(
354 functions=[_intensive_list_calc],
355 dependant_elements='elements'
356 )
357 machines_profile = attribute.Attribute(
358 functions=[_intensive_list_calc],
359 dependant_elements='elements'
360 )
361 lighting_profile = attribute.Attribute(
362 functions=[_intensive_list_calc],
363 dependant_elements='elements'
364 )
367class SBDisaggregationMixin:
368 guid_prefix = 'DisAgg_'
369 disaggregatable_classes: Set['BPSProduct'] = set()
370 thermal_zones = []
372 def __init__(self, disagg_parent: 'BPSProduct', sbs: list['SpaceBoundary']
373 , *args, **kwargs):
374 """
376 Args:
377 disagg_parent: Parent bim2sim element that was disaggregated
378 """
379 super().__init__(*args, **kwargs)
380 if self.disaggregatable_classes:
381 received = {type(disagg_parent)}
382 mismatch = received - self.disaggregatable_classes
383 if mismatch:
384 raise AssertionError("Can't aggregate %s from elements: %s" %
385 (self.__class__.__name__, mismatch))
386 self.thermal_zones = [sb.bound_thermal_zone for sb in sbs]
387 for tz in self.thermal_zones:
388 if disagg_parent in tz.bound_elements:
389 tz.bound_elements.remove(disagg_parent)
390 tz.bound_elements.append(self)
391 if sbs[0].related_bound and not isinstance(
392 sbs[0].related_bound, ExtSpatialSpaceBoundary):
393 # if the space boundary and its related_bound have different
394 # bound_elements which are assigned to have the same
395 # bound_element during disaggregation, the thermal zone must
396 # get a reference to the newly assigned bound_element instead.
397 if sbs[0].bound_element != sbs[0].related_bound.bound_element:
398 if (sbs[0].related_bound.bound_element in
399 sbs[0].related_bound.bound_thermal_zone.bound_elements):
400 sbs[0].related_bound.bound_thermal_zone.bound_elements.remove(
401 sbs[0].related_bound.bound_element)
402 sbs[0].related_bound.bound_thermal_zone.bound_elements.append(
403 self)
404 for sb in sbs:
405 # Only set disagg_parent if disagg_parent is the element of the SB
406 # because otherwise we prevent creation of disaggregations for this
407 # SB
408 if disagg_parent == sb.bound_element:
409 sb.disagg_parent = disagg_parent
410 sb.bound_element = self
411 # if sb.related_bound:
412 # if not isinstance(sb.related_bound, ExtSpatialSpaceBoundary):
413 # sb.related_bound.bound_element = self
414 # sb.related_bound.disagg_parent = disagg_parent
416 # set references to other elements
417 self.disagg_parent = disagg_parent
418 self.disagg_parent.disaggregations.append(self)
419 if len(sbs) > 2:
420 logger.error(f'More than 2 SBs detected here (GUID: {self}.')
421 if len(sbs) == 2:
422 if abs(sbs[0].net_bound_area - sbs[1].net_bound_area).m > 0.001:
423 logger.error(f'Large deviation in net bound area for SBs '
424 f'{sbs[0].guid} and {sbs[1].guid}')
425 if abs(sbs[0].bound_area - sbs[1].bound_area).m > 0.001:
426 logger.error(f'Large deviation in net bound area for SBs '
427 f'{sbs[0].guid} and {sbs[1].guid}')
429 # Get information from SB
430 self.space_boundaries = sbs
431 self.net_area = (
432 sbs[0].net_bound_area, AttributeDataSource.space_boundary)
433 self.gross_area = (
434 sbs[0].bound_area, AttributeDataSource.space_boundary)
435 self.opening_area = (
436 sbs[0].opening_area, AttributeDataSource.space_boundary)
437 # get information from disagg_parent
438 for att_name, value in disagg_parent.attributes.items():
439 if att_name not in ['net_area', 'gross_area', 'opening_area',
440 'gross_volume', 'net_volume']:
441 self.attributes[att_name] = value
442 self.layerset = disagg_parent.layerset
443 self.material = disagg_parent.material
444 self.material_set = disagg_parent.material_set
445 self.ifc = disagg_parent.ifc
446 self.storeys = disagg_parent.storeys
448 @staticmethod
449 def get_id(prefix=""):
450 prefix_length = len(prefix)
451 if prefix_length > 10:
452 raise AttributeError("Max prefix length is 10!")
453 ifcopenshell_guid = guid.new()[prefix_length+1:]
454 return f"{prefix}{ifcopenshell_guid}"
457class InnerFloorDisaggregated(SBDisaggregationMixin, InnerFloor):
458 disaggregatable_classes = {
459 InnerFloor, Slab, Roof, GroundFloor}
462class GroundFloorDisaggregated(SBDisaggregationMixin, GroundFloor):
463 disaggregatable_classes = {
464 InnerFloor, Slab, Roof, GroundFloor}
467class RoofDisaggregated(SBDisaggregationMixin, Roof):
468 disaggregatable_classes = {
469 InnerFloor, Slab, Roof, GroundFloor}
472class InnerWallDisaggregated(SBDisaggregationMixin, InnerWall):
473 disaggregatable_classes = {
474 Wall, OuterWall, InnerWall}
477class OuterWallDisaggregated(SBDisaggregationMixin, OuterWall):
478 disaggregatable_classes = {
479 Wall, OuterWall, InnerWall, InnerFloor}
482class InnerDoorDisaggregated(SBDisaggregationMixin, InnerDoor):
483 disaggregatable_classes = {
484 Door, OuterDoor, InnerDoor}
487class OuterDoorDisaggregated(SBDisaggregationMixin, OuterDoor):
488 disaggregatable_classes = {
489 Door, OuterDoor, InnerDoor}
492class WindowDisaggregated(SBDisaggregationMixin, Window):
493 disaggregatable_classes = {Window}