Coverage for bim2sim / elements / bps_elements.py: 49%
815 statements
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-18 09:34 +0000
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-18 09:34 +0000
1"""Module contains the different classes for all HVAC elements"""
2import inspect
3import logging
4import math
5import re
6import sys
7from datetime import date
8from typing import Set, List, Union
10import ifcopenshell
11import ifcopenshell.geom
12from OCC.Core.BRepBndLib import brepbndlib
13from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_Transform
14from OCC.Core.BRepExtrema import BRepExtrema_DistShapeShape
15from OCC.Core.BRepGProp import brepgprop
16from OCC.Core.BRepLib import BRepLib_FuseEdges
17from OCC.Core.Bnd import Bnd_Box
18from OCC.Core.Extrema import Extrema_ExtFlag_MIN
19from OCC.Core.GProp import GProp_GProps
20from OCC.Core.ShapeUpgrade import ShapeUpgrade_UnifySameDomain
21from OCC.Core.gp import gp_Trsf, gp_Vec, gp_XYZ, gp_Pnt, \
22 gp_Mat, gp_Quaternion
23from ifcopenshell import guid
25from bim2sim.elements.mapping import condition, attribute
26from bim2sim.elements.base_elements import ProductBased, RelationBased
27from bim2sim.elements.mapping.units import ureg
28from bim2sim.tasks.common.inner_loop_remover import remove_inner_loops
29from bim2sim.utilities.common_functions import vector_angle, angle_equivalent
30from bim2sim.utilities.pyocc_tools import PyOCCTools
31from bim2sim.utilities.types import IFCDomain, BoundaryOrientation
33logger = logging.getLogger(__name__)
36class BPSProduct(ProductBased):
37 domain = 'BPS'
39 def __init__(self, *args, **kwargs):
40 super().__init__(*args, **kwargs)
41 self.thermal_zones = []
42 self.space_boundaries = []
43 self.storeys = []
44 self.material = None
45 self.disaggregations = []
46 self.building = None
47 self.site = None
49 def __repr__(self):
50 return "<%s (guid: %s)>" % (
51 self.__class__.__name__, self.guid)
53 def get_bound_area(self, name) -> ureg.Quantity:
54 """ get gross bound area (including opening areas) of the element"""
55 return sum(sb.bound_area for sb in self.sbs_without_corresponding)
57 def get_net_bound_area(self, name) -> ureg.Quantity:
58 """get net area (including opening areas) of the element"""
59 return self.gross_area - self.opening_area
61 @property
62 def is_external(self) -> bool or None:
63 """Checks if the corresponding element has contact with external
64 environment (e.g. ground, roof, wall)"""
65 if hasattr(self, 'parent'):
66 return self.parent.is_external
67 elif hasattr(self, 'ifc'):
68 if hasattr(self.ifc, 'ProvidesBoundaries'):
69 if len(self.ifc.ProvidesBoundaries) > 0:
70 ext_int = list(
71 set([boundary.InternalOrExternalBoundary for boundary
72 in self.ifc.ProvidesBoundaries]))
73 if len(ext_int) == 1:
74 if ext_int[0].lower() == 'external':
75 return True
76 if ext_int[0].lower() == 'internal':
77 return False
78 else:
79 return ext_int
80 return None
82 def calc_cost_group(self) -> int:
83 """Default cost group for building elements is 300"""
84 return 300
86 def _calc_teaser_orientation(self, name) -> Union[int, None]:
87 """Calculate the orientation of the bps product based on SB direction.
89 For buildings elements we can use the more reliable space boundaries
90 normal vector to calculate the orientation if the space boundaries
91 exists. Otherwise the base calc_orientation of IFCBased will be used.
93 Returns:
94 Orientation angle between 0 and 360.
95 (0 : north, 90: east, 180: south, 270: west)
96 """
97 true_north = self.get_true_north()
98 if len(self.space_boundaries):
99 new_orientation = self.group_orientation(
100 [vector_angle(space_boundary.bound_normal.Coord())
101 for space_boundary in self.space_boundaries])
102 if new_orientation is not None:
103 return int(angle_equivalent(new_orientation + true_north))
104 # return int(angle_equivalent(super().calc_orientation() + true_north))
105 return None
107 @staticmethod
108 def group_orientation(orientations: list):
109 dict_orientations = {}
110 for orientation in orientations:
111 rounded_orientation = round(orientation)
112 if rounded_orientation not in dict_orientations:
113 dict_orientations[rounded_orientation] = 0
114 dict_orientations[rounded_orientation] += 1
115 if len(dict_orientations):
116 return max(dict_orientations, key=dict_orientations.get)
117 return None
119 def _get_sbs_without_corresponding(self, name) -> list:
120 """get a list with only not duplicated space boundaries"""
121 sbs_without_corresponding = list(self.space_boundaries)
122 for sb in self.space_boundaries:
123 if sb in sbs_without_corresponding:
124 if sb.related_bound and sb.related_bound in \
125 sbs_without_corresponding:
126 sbs_without_corresponding.remove(sb.related_bound)
127 return sbs_without_corresponding
129 def _get_opening_area(self, name):
130 """get sum of opening areas of the element"""
131 return sum(sb.opening_area for sb in self.sbs_without_corresponding)
133 teaser_orientation = attribute.Attribute(
134 description="Orientation of element in TEASER conventions. 0-360 for "
135 "orientation of vertical elements and -1 for roofs and "
136 "ceiling, -2 for groundfloors and floors.",
137 functions=[_calc_teaser_orientation],
138 )
140 gross_area = attribute.Attribute(
141 functions=[get_bound_area],
142 unit=ureg.meter ** 2
143 )
145 net_area = attribute.Attribute(
146 functions=[get_net_bound_area],
147 unit=ureg.meter ** 2
148 )
150 sbs_without_corresponding = attribute.Attribute(
151 description="A list with only not duplicated space boundaries",
152 functions=[_get_sbs_without_corresponding]
153 )
155 opening_area = attribute.Attribute(
156 description="Sum of opening areas of the element",
157 functions=[_get_opening_area]
158 )
161class ThermalZone(BPSProduct):
162 ifc_types = {
163 "IfcSpace":
164 ['*', 'SPACE', 'PARKING', 'GFA', 'INTERNAL', 'EXTERNAL']
165 }
167 pattern_ifc_type = [
168 re.compile('Space', flags=re.IGNORECASE),
169 re.compile('Zone', flags=re.IGNORECASE)
170 ]
172 def __init__(self, *args, **kwargs):
173 self.bound_elements = kwargs.pop('bound_elements', [])
174 super().__init__(*args, **kwargs)
176 @property
177 def outer_walls(self) -> list:
178 """List of all outer wall elements bounded to the thermal zone"""
179 return [
180 ele for ele in self.bound_elements if isinstance(ele, OuterWall)]
182 @property
183 def windows(self) -> list:
184 """List of all window elements bounded to the thermal zone"""
185 return [ele for ele in self.bound_elements if isinstance(ele, Window)]
187 @property
188 def is_external(self) -> bool:
189 """determines if a thermal zone is external or internal based on the
190 presence of outer walls"""
191 return len(self.outer_walls) > 0
193 def _get_external_orientation(self, name) -> str or float:
194 """determines the orientation of the thermal zone based on its elements
195 it can be a corner (list of 2 angles) or an edge (1 angle)"""
196 if self.is_external is True:
197 orientations = [ele.teaser_orientation for ele in self.outer_walls]
198 calc_temp = list(set(orientations))
199 sum_or = sum(calc_temp)
200 if 0 in calc_temp:
201 if sum_or > 180:
202 sum_or += 360
203 return sum_or / len(calc_temp)
204 return 'Internal'
206 def _get_glass_percentage(self, name) -> float or ureg.Quantity:
207 """determines the glass area/facade area ratio for all the windows in
208 the space in one of the 4 following ranges
209 0%-30%: 15
210 30%-50%: 40
211 50%-70%: 60
212 70%-100%: 85"""
213 glass_area = sum(wi.gross_area for wi in self.windows)
214 facade_area = sum(wa.gross_area for wa in self.outer_walls)
215 if facade_area > 0:
216 return 100 * (glass_area / (facade_area + glass_area)).m
217 else:
218 return 'Internal'
220 def _get_space_neighbors(self, name) -> list:
221 """determines the neighbors of the thermal zone"""
222 neighbors = []
223 for sb in self.space_boundaries:
224 if sb.related_bound is not None:
225 tz = sb.related_bound.bound_thermal_zone
226 # todo: check if computation of neighbors works as expected
227 # what if boundary has no related bound but still has a
228 # neighbor?
229 # hint: neighbors != related bounds
230 if (tz is not self) and (tz not in neighbors):
231 neighbors.append(tz)
232 return neighbors
234 def _get_space_shape(self, name):
235 """returns topods shape of the IfcSpace"""
236 settings = ifcopenshell.geom.settings()
237 settings.set(settings.USE_PYTHON_OPENCASCADE, True)
238 settings.set(settings.USE_WORLD_COORDS, True)
239 settings.set(settings.PRECISION, 1e-6)
240 settings.set(
241 "dimensionality",
242 ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS) # 2
243 # settings.set(settings.EXCLUDE_SOLIDS_AND_SURFACES, False)
244 # settings.set(settings.INCLUDE_CURVES, True)
245 return ifcopenshell.geom.create_shape(settings, self.ifc).geometry
247 def _get_space_center(self, name) -> float:
248 """
249 This function returns the center of the bounding box of an ifc space
250 shape
251 :return: center of space bounding box (gp_Pnt)
252 """
253 bbox = Bnd_Box()
254 brepbndlib.Add(self.space_shape, bbox)
255 bbox_center = ifcopenshell.geom.utils.get_bounding_box_center(bbox)
256 return bbox_center
258 def _get_footprint_shape(self, name):
259 """
260 This function returns the footprint of a space shape. This can be
261 used e.g., to visualize floor plans.
262 """
263 footprint = PyOCCTools.get_footprint_of_shape(self.space_shape)
264 return footprint
266 def _get_space_shape_volume(self, name):
267 """
268 This function returns the volume of a space shape
269 """
270 return PyOCCTools.get_shape_volume(self.space_shape)
272 def _get_volume_geometric(self, name):
273 """
274 This function returns the volume of a space geometrically
275 """
276 return self.gross_area * self.height
278 def _get_usage(self, name):
279 """
280 This function returns the usage of a space
281 """
282 if self.zone_name is not None:
283 usage = self.zone_name
284 elif self.ifc.LongName is not None and \
285 "oldSpaceGuids_" not in self.ifc.LongName:
286 # todo oldSpaceGuids_ is hardcode for erics tool
287 usage = self.ifc.LongName
288 else:
289 usage = self.name
290 return usage
292 def _get_name(self, name):
293 """
294 This function returns the name of a space
295 """
296 if self.zone_name:
297 space_name = self.zone_name
298 else:
299 space_name = self.ifc.Name
300 return space_name
302 def get_bound_floor_area(self, name):
303 """Get bound floor area of zone. This is currently set by sum of all
304 horizontal gross area and take half of it due to issues with
305 TOP BOTTOM"""
306 leveled_areas = {}
307 for height, sbs in self.horizontal_sbs.items():
308 if height not in leveled_areas:
309 leveled_areas[height] = 0
310 leveled_areas[height] += sum([sb.bound_area for sb in sbs])
312 return sum(leveled_areas.values()) / 2
314 def get_net_bound_floor_area(self, name):
315 """Get net bound floor area of zone. This is currently set by sum of all
316 horizontal net area and take half of it due to issues with TOP BOTTOM."""
317 leveled_areas = {}
318 for height, sbs in self.horizontal_sbs.items():
319 if height not in leveled_areas:
320 leveled_areas[height] = 0
321 leveled_areas[height] += sum([sb.net_bound_area for sb in sbs])
323 return sum(leveled_areas.values()) / 2
325 def _get_horizontal_sbs(self, name):
326 """get all horizonal SBs in a zone and convert them into a dict with
327 key z-height in room and the SB as value."""
328 # todo: use only bottom when TOP bottom is working correctly
329 valid = [BoundaryOrientation.top, BoundaryOrientation.bottom]
330 leveled_sbs = {}
331 for sb in self.sbs_without_corresponding:
332 if sb.top_bottom in valid:
333 pos = round(sb.position[2], 1)
334 if pos not in leveled_sbs:
335 leveled_sbs[pos] = []
336 leveled_sbs[pos].append(sb)
338 return leveled_sbs
340 def _area_specific_post_processing(self, value):
341 return value / self.net_area
343 def _get_heating_profile(self, name) -> list:
344 """returns a heating profile using the heat temperature in the IFC"""
345 # todo make this "dynamic" with a night set back
346 if self.t_set_heat is not None:
347 return [self.t_set_heat.to(ureg.kelvin).m] * 24
349 def _get_cooling_profile(self, name) -> list:
350 """returns a cooling profile using the cool temperature in the IFC"""
351 # todo make this "dynamic" with a night set back
352 if self.t_set_cool is not None:
353 return [self.t_set_cool.to(ureg.kelvin).m] * 24
355 def _get_persons(self, name):
356 if self.area_per_occupant:
357 return 1 / self.area_per_occupant
359 external_orientation = attribute.Attribute(
360 description="Orientation of the thermal zone, either 'Internal' or a "
361 "list of 2 angles or a single angle as value between 0 and "
362 "360.",
363 functions=[_get_external_orientation]
364 )
366 glass_percentage = attribute.Attribute(
367 description="Determines the glass area/facade area ratio for all the "
368 "windows in the space in one of the 4 following ranges:"
369 " 0%-30%: 15, 30%-50%: 40, 50%-70%: 60, 70%-100%: 85.",
370 functions=[_get_glass_percentage]
371 )
373 space_neighbors = attribute.Attribute(
374 description="Determines the neighbors of the thermal zone.",
375 functions=[_get_space_neighbors]
376 )
378 space_shape = attribute.Attribute(
379 description="Returns topods shape of the IfcSpace.",
380 functions=[_get_space_shape]
381 )
383 space_center = attribute.Attribute(
384 description="Returns the center of the bounding box of an ifc space "
385 "shape.",
386 functions=[_get_space_center]
387 )
389 footprint_shape = attribute.Attribute(
390 description="Returns the footprint of a space shape, which can be "
391 "used e.g., to visualize floor plans.",
392 functions=[_get_footprint_shape]
393 )
395 horizontal_sbs = attribute.Attribute(
396 description="All horizontal space boundaries in a zone as dict. Key is"
397 " the z-zeight in the room and value the SB.",
398 functions=[_get_horizontal_sbs]
399 )
401 zone_name = attribute.Attribute(
402 default_ps=("Pset_SpaceCommon", "Reference")
403 )
405 name = attribute.Attribute(
406 functions=[_get_name]
407 )
409 usage = attribute.Attribute(
410 default_ps=("Pset_SpaceOccupancyRequirements", "OccupancyType"),
411 functions=[_get_usage]
412 )
414 t_set_heat = attribute.Attribute(
415 default_ps=("Pset_SpaceThermalRequirements", "SpaceTemperatureMin"),
416 unit=ureg.degC,
417 )
419 t_set_cool = attribute.Attribute(
420 default_ps=("Pset_SpaceThermalRequirements", "SpaceTemperatureMax"),
421 unit=ureg.degC,
422 )
424 t_ground = attribute.Attribute(
425 unit=ureg.degC,
426 default=13,
427 )
429 max_humidity = attribute.Attribute(
430 default_ps=("Pset_SpaceThermalRequirements", "SpaceHumidityMax"),
431 unit=ureg.dimensionless,
432 )
434 min_humidity = attribute.Attribute(
435 default_ps=("Pset_SpaceThermalRequirements", "SpaceHumidityMin"),
436 unit=ureg.dimensionless,
437 )
439 natural_ventilation = attribute.Attribute(
440 default_ps=("Pset_SpaceThermalRequirements", "NaturalVentilation"),
441 )
443 natural_ventilation_rate = attribute.Attribute(
444 default_ps=("Pset_SpaceThermalRequirements", "NaturalVentilationRate"),
445 unit=1 / ureg.hour,
446 )
448 mechanical_ventilation_rate = attribute.Attribute(
449 default_ps=("Pset_SpaceThermalRequirements",
450 "MechanicalVentilationRate"),
451 unit=1 / ureg.hour,
452 )
454 with_ahu = attribute.Attribute(
455 default_ps=("Pset_SpaceThermalRequirements", "AirConditioning"),
456 )
458 central_ahu = attribute.Attribute(
459 default_ps=("Pset_SpaceThermalRequirements", "AirConditioningCentral"),
460 )
462 gross_area = attribute.Attribute(
463 default_ps=("Qto_SpaceBaseQuantities", "GrossFloorArea"),
464 functions=[get_bound_floor_area],
465 unit=ureg.meter ** 2
466 )
468 net_area = attribute.Attribute(
469 default_ps=("Qto_SpaceBaseQuantities", "NetFloorArea"),
470 functions=[get_net_bound_floor_area],
471 unit=ureg.meter ** 2
472 )
474 net_wall_area = attribute.Attribute(
475 default_ps=("Qto_SpaceBaseQuantities", "NetWallArea"),
476 unit=ureg.meter ** 2
477 )
479 net_ceiling_area = attribute.Attribute(
480 default_ps=("Qto_SpaceBaseQuantities", "NetCeilingArea"),
481 unit=ureg.meter ** 2
482 )
484 net_volume = attribute.Attribute(
485 default_ps=("Qto_SpaceBaseQuantities", "NetVolume"),
486 functions=[_get_space_shape_volume, _get_volume_geometric],
487 unit=ureg.meter ** 3,
488 )
489 gross_volume = attribute.Attribute(
490 default_ps=("Qto_SpaceBaseQuantities", "GrossVolume"),
491 functions=[_get_volume_geometric],
492 unit=ureg.meter ** 3,
493 )
495 height = attribute.Attribute(
496 default_ps=("Qto_SpaceBaseQuantities", "Height"),
497 unit=ureg.meter,
498 )
500 length = attribute.Attribute(
501 default_ps=("Qto_SpaceBaseQuantities", "Length"),
502 unit=ureg.meter,
503 )
505 width = attribute.Attribute(
506 default_ps=("Qto_SpaceBaseQuantities", "Width"),
507 unit=ureg.m
508 )
510 area_per_occupant = attribute.Attribute(
511 default_ps=("Pset_SpaceOccupancyRequirements", "AreaPerOccupant"),
512 unit=ureg.meter ** 2
513 )
515 space_shape_volume = attribute.Attribute(
516 functions=[_get_space_shape_volume],
517 unit=ureg.meter ** 3,
518 )
520 clothing_persons = attribute.Attribute(
521 default_ps=("", "")
522 )
524 surround_clo_persons = attribute.Attribute(
525 default_ps=("", "")
526 )
528 heating_profile = attribute.Attribute(
529 functions=[_get_heating_profile],
530 )
532 cooling_profile = attribute.Attribute(
533 functions=[_get_cooling_profile],
534 )
536 persons = attribute.Attribute(
537 functions=[_get_persons],
538 )
540 # use conditions
541 with_cooling = attribute.Attribute(
542 )
544 with_heating = attribute.Attribute(
545 )
547 T_threshold_heating = attribute.Attribute(
548 )
550 activity_degree_persons = attribute.Attribute(
551 )
553 fixed_heat_flow_rate_persons = attribute.Attribute(
554 default_ps=("Pset_SpaceThermalLoad", "People"),
555 unit=ureg.W,
556 )
558 internal_gains_moisture_no_people = attribute.Attribute(
559 )
561 T_threshold_cooling = attribute.Attribute(
562 )
564 ratio_conv_rad_persons = attribute.Attribute(
565 default=0.5,
566 )
568 ratio_conv_rad_machines = attribute.Attribute(
569 default=0.5,
570 )
572 ratio_conv_rad_lighting = attribute.Attribute(
573 default=0.5,
574 )
576 machines = attribute.Attribute(
577 description="Specific internal gains through machines, if taken from"
578 " IFC property set a division by thermal zone area is"
579 " needed.",
580 default_ps=("Pset_SpaceThermalLoad", "EquipmentSensible"),
581 ifc_postprocessing=_area_specific_post_processing,
582 unit=ureg.W / (ureg.meter ** 2),
583 )
585 def _calc_lighting_power(self, name) -> float:
586 if self.use_maintained_illuminance:
587 return self.maintained_illuminance / self.lighting_efficiency_lumen
588 else:
589 return self.fixed_lighting_power
591 lighting_power = attribute.Attribute(
592 description="Specific lighting power in W/m2. If taken from IFC"
593 " property set a division by thermal zone area is needed.",
594 default_ps=("Pset_SpaceThermalLoad", "Lighting"),
595 ifc_postprocessing=_area_specific_post_processing,
596 functions=[_calc_lighting_power],
597 unit=ureg.W / (ureg.meter ** 2),
598 )
600 fixed_lighting_power = attribute.Attribute(
601 description="Specific fixed electrical power for lighting in W/m2. "
602 "This value is taken from SIA 2024.",
603 unit=ureg.W / (ureg.meter ** 2)
604 )
606 maintained_illuminance = attribute.Attribute(
607 description="Maintained illuminance value for lighting. This value is"
608 " taken from SIA 2024.",
609 unit=ureg.lumen / (ureg.meter ** 2)
610 )
612 use_maintained_illuminance = attribute.Attribute(
613 description="Decision variable to determine if lighting_power will"
614 " be given by fixed_lighting_power or by calculation "
615 "using the variables maintained_illuminance and "
616 "lighting_efficiency_lumen. This is not available in IFC "
617 "and can be set through the sim_setting with equivalent "
618 "name. "
619 )
621 lighting_efficiency_lumen = attribute.Attribute(
622 description="Lighting efficiency in lm/W_el, in german: Lichtausbeute.",
623 unit=ureg.lumen / ureg.W
624 )
626 use_constant_infiltration = attribute.Attribute(
627 )
629 base_infiltration = attribute.Attribute(
630 )
632 max_user_infiltration = attribute.Attribute(
633 )
635 max_overheating_infiltration = attribute.Attribute(
636 )
638 max_summer_infiltration = attribute.Attribute(
639 )
641 winter_reduction_infiltration = attribute.Attribute(
642 )
644 min_ahu = attribute.Attribute(
645 )
647 max_ahu = attribute.Attribute(
648 default_ps=("Pset_AirSideSystemInformation", "TotalAirflow"),
649 unit=ureg.meter ** 3 / ureg.s
650 )
652 with_ideal_thresholds = attribute.Attribute(
653 )
655 persons_profile = attribute.Attribute(
656 )
658 machines_profile = attribute.Attribute(
659 )
661 lighting_profile = attribute.Attribute(
662 )
664 def get__elements_by_type(self, type):
665 raise NotImplementedError
667 def __repr__(self):
668 return "<%s (usage: %s)>" \
669 % (self.__class__.__name__, self.usage)
671class ExternalSpatialElement(ThermalZone):
672 ifc_types = {
673 "IfcExternalSpatialElement":
674 ['*']
675 }
678class SpaceBoundary(RelationBased):
679 ifc_types = {'IfcRelSpaceBoundary': ['*']}
681 def __init__(self, *args, elements: dict, **kwargs):
682 """spaceboundary __init__ function"""
683 super().__init__(*args, **kwargs)
684 self.disaggregation = []
685 self.bound_element = None
686 self.disagg_parent = None
687 self.bound_thermal_zone = None
688 self._elements = elements
689 self.parent_bound = None
690 self.opening_bounds = []
692 def _calc_position(self, name):
693 """
694 calculates the position of the spaceboundary, using the relative
695 position of resultant disaggregation
696 """
697 if hasattr(self.ifc.ConnectionGeometry.SurfaceOnRelatingElement,
698 'BasisSurface'):
699 position = self.ifc.ConnectionGeometry.SurfaceOnRelatingElement. \
700 BasisSurface.Position.Location.Coordinates
701 else:
702 position = self.ifc.ConnectionGeometry.SurfaceOnRelatingElement. \
703 Position.Location.Coordinates
705 return position
707 @classmethod
708 def pre_validate(cls, ifc) -> bool:
709 return True
711 def validate_creation(self) -> bool:
712 if self.bound_area and self.bound_area < 1e-2 * ureg.meter ** 2:
713 return True
714 return False
716 def get_bound_area(self, name) -> ureg.Quantity:
717 """compute area of a space boundary"""
718 bound_prop = GProp_GProps()
719 brepgprop.SurfaceProperties(self.bound_shape, bound_prop)
720 area = bound_prop.Mass()
721 return area * ureg.meter ** 2
723 bound_area = attribute.Attribute(
724 description="The area bound by the space boundary.",
725 unit=ureg.meter ** 2,
726 functions=[get_bound_area]
727 )
729 def _get_top_bottom(self, name) -> BoundaryOrientation:
730 """
731 Determines if a boundary is a top (ceiling/roof) or bottom (floor/slab)
732 element based solely on its normal vector orientation.
734 Classification is based on the dot product between the boundary's
735 normal vector and the vertical vector (0, 0, 1):
736 - TOP: when normal points upward (dot product > cos(89°))
737 - BOTTOM: when normal points downward (dot product < cos(91°))
738 - VERTICAL: when normal is perpendicular to vertical (dot product ≈ 0)
740 Returns:
741 BoundaryOrientation: Enumerated orientation classification
742 """
743 vertical_vector = gp_XYZ(0.0, 0.0, 1.0)
744 cos_angle_top = math.cos(math.radians(89))
745 cos_angle_bottom = math.cos(math.radians(91))
747 normal_dot_vertical = vertical_vector.Dot(self.bound_normal)
749 # Classify based on dot product
750 if normal_dot_vertical > cos_angle_top:
751 return BoundaryOrientation.top
752 elif normal_dot_vertical < cos_angle_bottom:
753 return BoundaryOrientation.bottom
755 return BoundaryOrientation.vertical
757 def _get_bound_center(self, name):
758 """ compute center of the bounding box of a space boundary"""
759 p = GProp_GProps()
760 brepgprop.SurfaceProperties(self.bound_shape, p)
761 return p.CentreOfMass().XYZ()
763 def _get_related_bound(self, name):
764 """
765 Get corresponding space boundary in another space,
766 ensuring that corresponding space boundaries have a matching number of
767 vertices.
768 """
769 if hasattr(self.ifc, 'CorrespondingBoundary') and \
770 self.ifc.CorrespondingBoundary is not None:
771 corr_bound = self._elements.get(
772 self.ifc.CorrespondingBoundary.GlobalId)
773 if corr_bound:
774 nb_vert_this = PyOCCTools.get_number_of_vertices(
775 self.bound_shape)
776 nb_vert_other = PyOCCTools.get_number_of_vertices(
777 corr_bound.bound_shape)
778 # if not nb_vert_this == nb_vert_other:
779 # print("NO VERT MATCH!:", nb_vert_this, nb_vert_other)
780 if nb_vert_this == nb_vert_other:
781 return corr_bound
782 else:
783 # deal with a mismatch of vertices, due to different
784 # triangulation or for other reasons. Only applicable for
785 # small differences in the bound area between the
786 # corresponding surfaces
787 if abs(self.bound_area.m - corr_bound.bound_area.m) < 0.01:
788 # get points of the current space boundary
789 p = PyOCCTools.get_points_of_face(self.bound_shape)
790 # reverse the points and create a new face. Points
791 # have to be reverted, otherwise it would result in an
792 # incorrectly oriented surface normal
793 p.reverse()
794 new_corr_shape = PyOCCTools.make_faces_from_pnts(p)
795 # move the new shape of the corresponding boundary to
796 # the original position of the corresponding boundary
797 new_moved_corr_shape = (
798 PyOCCTools.move_bounds_to_vertical_pos([
799 new_corr_shape], corr_bound.bound_shape))[0]
800 # assign the new shape to the original shape and
801 # return the new corresponding boundary
802 corr_bound.bound_shape = new_moved_corr_shape
803 return corr_bound
804 if self.bound_element is None:
805 # return None
806 # check for virtual bounds
807 if not self.physical:
808 corr_bound = None
809 # cover virtual space boundaries without related IfcVirtualElement
810 if not self.ifc.RelatedBuildingElement:
811 vbs = [b for b in self._elements.values() if
812 isinstance(b, SpaceBoundary) and not
813 b.ifc.RelatedBuildingElement]
814 for b in vbs:
815 if b is self:
816 continue
817 if b.ifc.RelatingSpace == self.ifc.RelatingSpace:
818 continue
819 if not (b.bound_area.m - self.bound_area.m) ** 2 < 1e-2:
820 continue
821 center_dist = gp_Pnt(self.bound_center).Distance(
822 gp_Pnt(b.bound_center)) ** 2
823 if center_dist > 0.5:
824 continue
825 corr_bound = b
826 return corr_bound
827 return None
828 # cover virtual space boundaries related to an IfcVirtualElement
829 if self.ifc.RelatedBuildingElement.is_a('IfcVirtualElement'):
830 if len(self.ifc.RelatedBuildingElement.ProvidesBoundaries) == 2:
831 for bound in self.ifc.RelatedBuildingElement.ProvidesBoundaries:
832 if bound.GlobalId != self.ifc.GlobalId:
833 corr_bound = self._elements[bound.GlobalId]
834 return corr_bound
835 elif len(self.bound_element.space_boundaries) == 1:
836 return None
837 elif len(self.bound_element.space_boundaries) >= 2:
838 own_space_id = self.bound_thermal_zone.ifc.GlobalId
839 min_dist = 1000
840 corr_bound = None
841 for bound in self.bound_element.space_boundaries:
842 if bound.level_description != "2a":
843 continue
844 if bound is self:
845 continue
846 # if bound.bound_normal.Dot(self.bound_normal) != -1:
847 # continue
848 other_area = bound.bound_area
849 if (other_area.m - self.bound_area.m) ** 2 > 1e-1:
850 continue
851 center_dist = gp_Pnt(self.bound_center).Distance(
852 gp_Pnt(bound.bound_center)) ** 2
853 if abs(center_dist) > 0.5:
854 continue
855 distance = BRepExtrema_DistShapeShape(
856 bound.bound_shape,
857 self.bound_shape,
858 Extrema_ExtFlag_MIN
859 ).Value()
860 if distance > min_dist:
861 continue
862 min_dist = abs(center_dist)
863 # self.check_for_vertex_duplicates(bound)
864 nb_vert_this = PyOCCTools.get_number_of_vertices(
865 self.bound_shape)
866 nb_vert_other = PyOCCTools.get_number_of_vertices(
867 bound.bound_shape)
868 # if not nb_vert_this == nb_vert_other:
869 # print("NO VERT MATCH!:", nb_vert_this, nb_vert_other)
870 if nb_vert_this == nb_vert_other:
871 corr_bound = bound
872 return corr_bound
873 else:
874 return None
876 def _get_related_adb_bound(self, name):
877 adb_bound = None
878 if self.bound_element is None:
879 return None
880 # check for visual bounds
881 if not self.physical:
882 return None
883 if self.related_bound:
884 if self.bound_thermal_zone == self.related_bound.bound_thermal_zone:
885 adb_bound = self.related_bound
886 return adb_bound
887 for bound in self.bound_element.space_boundaries:
888 if bound == self:
889 continue
890 if not bound.bound_thermal_zone == self.bound_thermal_zone:
891 continue
892 if abs(bound.bound_area.m - self.bound_area.m) > 1e-3:
893 continue
894 if all([abs(i) < 1e-3 for i in
895 ((self.bound_normal - bound.bound_normal).Coord())]):
896 continue
897 if gp_Pnt(bound.bound_center).Distance(
898 gp_Pnt(self.bound_center)) < 0.4:
899 adb_bound = bound
900 return adb_bound
902 related_adb_bound = attribute.Attribute(
903 description="Related adiabatic boundary.",
904 functions=[_get_related_adb_bound]
905 )
907 def _get_is_physical(self, name) -> bool:
908 """
909 This function returns True if the spaceboundary is physical
910 """
911 return self.ifc.PhysicalOrVirtualBoundary.lower() == 'physical'
913 def _get_bound_shape(self, name):
914 settings = ifcopenshell.geom.settings()
915 settings.set(settings.USE_PYTHON_OPENCASCADE, True)
916 settings.set(settings.USE_WORLD_COORDS, True)
917 settings.set(settings.PRECISION, 1e-6)
918 settings.set(
919 "dimensionality",
920 ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS) # 2
921 # settings.set(settings.EXCLUDE_SOLIDS_AND_SURFACES, False)
922 # settings.set(settings.INCLUDE_CURVES, True)
924 # check if the space boundary shapes need a unit conversion (i.e.,
925 # an additional transformation to the correct size and position)
926 length_unit = self.ifc_units.get('IfcLengthMeasure'.lower())
927 conv_required = length_unit != ureg.meter
929 try:
930 sore = self.ifc.ConnectionGeometry.SurfaceOnRelatingElement
931 # if sore.get_info()["InnerBoundaries"] is None:
932 if sore.InnerBoundaries is None:
933 sore.InnerBoundaries = ()
934 shape = ifcopenshell.geom.create_shape(settings, sore)
935 if sore.InnerBoundaries:
936 # shape = remove_inner_loops(shape) # todo: return None if not horizontal shape
937 # if not shape:
938 if self.bound_element.ifc.is_a(
939 'IfcWall'): # todo: remove this hotfix (generalize)
940 ifc_new = ifcopenshell.file()
941 temp_sore = ifc_new.create_entity('IfcCurveBoundedPlane',
942 OuterBoundary=sore.OuterBoundary,
943 BasisSurface=sore.BasisSurface)
944 temp_sore.InnerBoundaries = ()
945 shape = ifcopenshell.geom.create_shape(settings, temp_sore)
946 else:
947 # hotfix: ifcopenshell 0.8.4 does not sufficiently produce
948 # faces with inner holes (as opposed to ifcopenshell
949 # 0.7.0). This workaround "manually" performs a boolean
950 # operation to generate a TopoDS_Shape with inner holes
951 # before removing the inner loops with the dedicated
952 # inner_loop_remover function
953 ### START OF HOTFIX #####
954 inners = []
955 for inn in sore.InnerBoundaries:
956 ifc_new = ifcopenshell.file()
957 temp_sore = ifc_new.create_entity(
958 'IfcCurveBoundedPlane',
959 OuterBoundary=inn,
960 BasisSurface=sore.BasisSurface)
961 temp_sore.InnerBoundaries = ()
962 compound = ifcopenshell.geom.create_shape(settings,
963 temp_sore)
964 faces = PyOCCTools.get_face_from_shape(compound)
965 inners.append(faces)
966 sore.InnerBoundaries = ()
967 outer_shape_data = ifcopenshell.geom.create_shape(settings,
968 sore)
969 shape = PyOCCTools.triangulate_bound_shape(
970 outer_shape_data, inners)
971 #### END OF HOTFIX ####
972 shape = remove_inner_loops(shape)
973 if not (sore.InnerBoundaries and not self.bound_element.ifc.is_a(
974 'IfcWall')):
975 faces = PyOCCTools.get_faces_from_shape(shape)
976 if len(faces) > 1:
977 unify = ShapeUpgrade_UnifySameDomain()
978 unify.Initialize(shape)
979 unify.Build()
980 shape = unify.Shape()
981 faces = PyOCCTools.get_faces_from_shape(shape)
982 face = faces[0]
983 face = PyOCCTools.remove_coincident_and_collinear_points_from_face(
984 face)
985 shape = face
986 except:
987 try:
988 sore = self.ifc.ConnectionGeometry.SurfaceOnRelatingElement
989 ifc_new = ifcopenshell.file()
990 temp_sore = ifc_new.create_entity('IfcCurveBoundedPlane',
991 OuterBoundary=sore.OuterBoundary,
992 BasisSurface=sore.BasisSurface)
993 temp_sore.InnerBoundaries = ()
994 shape = ifcopenshell.geom.create_shape(settings, temp_sore)
995 except:
996 poly = self.ifc.ConnectionGeometry.SurfaceOnRelatingElement.OuterBoundary.Points
997 pnts = []
998 for p in poly:
999 p.Coordinates = (p.Coordinates[0], p.Coordinates[1], 0.0)
1000 pnts.append((p.Coordinates[:]))
1001 shape = PyOCCTools.make_faces_from_pnts(pnts)
1002 shape = BRepLib_FuseEdges(shape).Shape()
1004 if conv_required:
1005 # scale newly created shape of space boundary to correct size
1006 conv_factor = (1 * length_unit).to(
1007 ureg.metre).m
1008 # shape scaling seems to be covered by ifcopenshell, obsolete
1009 # shape = PyOCCTools.scale_shape(shape, conv_factor, gp_Pnt(0, 0,
1010 # 0))
1012 if self.ifc.RelatingSpace.ObjectPlacement:
1013 lp = PyOCCTools.local_placement(
1014 self.ifc.RelatingSpace.ObjectPlacement).tolist()
1015 # transform newly created shape of space boundary to correct
1016 # position if a unit conversion is required.
1017 if conv_required:
1018 for i in range(len(lp)):
1019 for j in range(len(lp[i])):
1020 coord = lp[i][j] * length_unit
1021 lp[i][j] = coord.to(ureg.meter).m
1022 mat = gp_Mat(lp[0][0], lp[0][1], lp[0][2], lp[1][0], lp[1][1],
1023 lp[1][2], lp[2][0], lp[2][1], lp[2][2])
1024 vec = gp_Vec(lp[0][3], lp[1][3], lp[2][3])
1025 trsf = gp_Trsf()
1026 trsf.SetTransformation(gp_Quaternion(mat), vec)
1027 shape = BRepBuilderAPI_Transform(shape, trsf).Shape()
1029 # shape = shape.Reversed()
1030 unify = ShapeUpgrade_UnifySameDomain()
1031 unify.Initialize(shape)
1032 unify.Build()
1033 shape = unify.Shape()
1035 if self.bound_element is not None:
1036 bi = self.bound_element
1037 if not hasattr(bi, "related_openings"):
1038 return shape
1039 if len(bi.related_openings) == 0:
1040 return shape
1041 shape = PyOCCTools.get_face_from_shape(shape)
1042 return shape
1044 def get_level_description(self, name) -> str:
1045 """
1046 This function returns the level description of the spaceboundary
1047 """
1048 return self.ifc.Description
1050 def _get_is_external(self, name) -> Union[None, bool]:
1051 """
1052 This function returns True if the spaceboundary is external
1053 """
1054 if self.ifc.InternalOrExternalBoundary is not None:
1055 ifc_ext_internal = self.ifc.InternalOrExternalBoundary.lower()
1056 if ifc_ext_internal == 'internal':
1057 return False
1058 elif 'external' in ifc_ext_internal:
1059 return True
1060 else:
1061 return None
1062 # return not self.ifc.InternalOrExternalBoundary.lower() == 'internal'
1064 def _get_opening_area(self, name):
1065 """
1066 This function returns the opening area of the spaceboundary
1067 """
1068 if self.opening_bounds:
1069 return sum(opening_boundary.bound_area for opening_boundary
1070 in self.opening_bounds)
1071 return 0
1073 def _get_net_bound_area(self, name):
1074 """
1075 This function returns the net bound area of the spaceboundary
1076 """
1077 return self.bound_area - self.opening_area
1079 is_external = attribute.Attribute(
1080 description="True if the Space Boundary is external",
1081 functions=[_get_is_external]
1082 )
1084 bound_shape = attribute.Attribute(
1085 description="Bound shape element of the SB.",
1086 functions=[_get_bound_shape]
1087 )
1089 top_bottom = attribute.Attribute(
1090 description="Info if the SB is top "
1091 "(ceiling etc.) or bottom (floor etc.).",
1092 functions=[_get_top_bottom]
1093 )
1095 bound_center = attribute.Attribute(
1096 description="The center of the space boundary.",
1097 functions=[_get_bound_center]
1098 )
1100 related_bound = attribute.Attribute(
1101 description="Related space boundary.",
1102 functions=[_get_related_bound]
1103 )
1105 physical = attribute.Attribute(
1106 description="If the Space Boundary is physical or not.",
1107 functions=[_get_is_physical]
1108 )
1110 opening_area = attribute.Attribute(
1111 description="Opening area of the Space Boundary.",
1112 functions = [_get_opening_area]
1113 )
1115 net_bound_area = attribute.Attribute(
1116 description="Net bound area of the Space Boundary",
1117 functions=[_get_net_bound_area]
1118 )
1120 def _get_bound_normal(self, name):
1121 """
1122 This function returns the normal vector of the spaceboundary
1123 """
1124 return PyOCCTools.simple_face_normal(self.bound_shape)
1126 bound_normal = attribute.Attribute(
1127 description="Normal vector of the Space Boundary.",
1128 functions=[_get_bound_normal]
1129 )
1131 level_description = attribute.Attribute(
1132 functions=[get_level_description],
1133 # Todo this should be removed in near future. We should either
1134 # find # a way to distinguish the level of SB by something
1135 # different or should check this during the creation of SBs
1136 # and throw an error if the level is not defined.
1137 default='2a'
1138 # HACK: Rou's Model has 2a boundaries but, the description is None,
1139 # default set to 2a to temporary solve this problem
1140 )
1142 internal_external_type = attribute.Attribute(
1143 description="Defines, whether the Space Boundary is internal"
1144 " (Internal), or external, i.e. adjacent to open space "
1145 "(that can be an partially enclosed space, such as terrace"
1146 " (External",
1147 ifc_attr_name="InternalOrExternalBoundary"
1148 )
1151class ExtSpatialSpaceBoundary(SpaceBoundary):
1152 """describes all space boundaries related to an IfcExternalSpatialElement instead of an IfcSpace"""
1153 pass
1156class SpaceBoundary2B(SpaceBoundary):
1157 """describes all newly created space boundaries of type 2b to fill gaps within spaces"""
1159 def __init__(self, *args, elements=None, **kwargs):
1160 super(SpaceBoundary2B, self).__init__(*args, elements=None, **kwargs)
1161 self.ifc = ifcopenshell.create_entity('IfcRelSpaceBoundary')
1162 self.guid = None
1163 self.bound_shape = None
1164 self.thermal_zones = []
1165 self.bound_element = None
1166 self.physical = True
1167 self.is_external = False
1168 self.related_bound = None
1169 self.related_adb_bound = None
1170 self.level_description = '2b'
1173class BPSProductWithLayers(BPSProduct):
1174 ifc_types = {}
1176 def __init__(self, *args, **kwargs):
1177 """BPSProductWithLayers __init__ function.
1179 Convention in bim2sim for layerset is layer 0 is inside,
1180 layer n is outside.
1181 """
1182 super().__init__(*args, **kwargs)
1183 self.layerset = None
1185 def get_u_value(self, name):
1186 """wall get_u_value function"""
1187 layers_r = 0
1188 for layer in self.layerset.layers:
1189 if layer.thickness:
1190 if layer.material.thermal_conduc and \
1191 layer.material.thermal_conduc > 0:
1192 layers_r += layer.thickness / layer.material.thermal_conduc
1194 if layers_r > 0:
1195 return 1 / layers_r
1196 return None
1198 def get_thickness_by_layers(self, name):
1199 """calculate the total thickness of the product based on the thickness
1200 of each layer."""
1201 thickness = 0
1202 for layer in self.layerset.layers:
1203 if layer.thickness:
1204 thickness += layer.thickness
1205 return thickness
1208class Wall(BPSProductWithLayers):
1209 """Abstract wall class, only its subclasses Inner- and Outerwalls are used.
1211 Every element where self.is_external is not True, is an InnerWall.
1212 """
1213 ifc_types = {
1214 "IfcWall":
1215 ['*', 'MOVABLE', 'PARAPET', 'PARTITIONING', 'PLUMBINGWALL',
1216 'SHEAR', 'SOLIDWALL', 'POLYGONAL', 'DOOR', 'GATE', 'TRAPDOOR'],
1217 "IfcWallStandardCase":
1218 ['*', 'MOVABLE', 'PARAPET', 'PARTITIONING', 'PLUMBINGWALL',
1219 'SHEAR', 'SOLIDWALL', 'POLYGONAL', 'DOOR', 'GATE', 'TRAPDOOR'],
1220 "IfcColumn": ['*'], # Hotfix. TODO: Implement appropriate classes
1221 "IfcCurtainWall": ['*'] # Hotfix. TODO: Implement appropriate classes
1222 # "IfcElementedCase": "?" # TODO
1223 }
1225 conditions = [
1226 condition.RangeCondition('u_value',
1227 0 * ureg.W / ureg.K / ureg.meter ** 2,
1228 5 * ureg.W / ureg.K / ureg.meter ** 2,
1229 critical_for_creation=False),
1230 condition.UValueCondition('u_value',
1231 threshold=0.2,
1232 critical_for_creation=False),
1233 ]
1235 pattern_ifc_type = [
1236 re.compile('Wall', flags=re.IGNORECASE),
1237 re.compile('Wand', flags=re.IGNORECASE)
1238 ]
1240 def __init__(self, *args, **kwargs):
1241 """wall __init__ function"""
1242 super().__init__(*args, **kwargs)
1244 def get_better_subclass(self):
1245 return OuterWall if self.is_external else InnerWall
1247 net_area = attribute.Attribute(
1248 default_ps=("Qto_WallBaseQuantities", "NetSideArea"),
1249 functions=[BPSProduct.get_net_bound_area],
1250 unit=ureg.meter ** 2
1251 )
1253 gross_area = attribute.Attribute(
1254 default_ps=("Qto_WallBaseQuantities", "GrossSideArea"),
1255 functions=[BPSProduct.get_bound_area],
1256 unit=ureg.meter ** 2
1257 )
1259 tilt = attribute.Attribute(
1260 default=90
1261 )
1263 u_value = attribute.Attribute(
1264 default_ps=("Pset_WallCommon", "ThermalTransmittance"),
1265 unit=ureg.W / ureg.K / ureg.meter ** 2,
1266 functions=[BPSProductWithLayers.get_u_value],
1267 )
1269 width = attribute.Attribute(
1270 default_ps=("Qto_WallBaseQuantities", "Width"),
1271 functions=[BPSProductWithLayers.get_thickness_by_layers],
1272 unit=ureg.m
1273 )
1275 inner_convection = attribute.Attribute(
1276 unit=ureg.W / ureg.K / ureg.meter ** 2,
1277 default=0.6
1278 )
1280 is_load_bearing = attribute.Attribute(
1281 default_ps=("Pset_WallCommon", "LoadBearing"),
1282 )
1284 net_volume = attribute.Attribute(
1285 default_ps=("Qto_WallBaseQuantities", "NetVolume"),
1286 unit=ureg.meter ** 3
1287 )
1289 gross_volume = attribute.Attribute(
1290 default_ps=("Qto_WallBaseQuantities", "GrossVolume")
1291 )
1294class Layer(BPSProduct):
1295 """Represents the IfcMaterialLayer class."""
1296 ifc_types = {
1297 "IfcMaterialLayer": ["*"],
1298 }
1299 guid_prefix = "Layer_"
1301 conditions = [
1302 condition.RangeCondition('thickness',
1303 0 * ureg.m,
1304 10 * ureg.m,
1305 critical_for_creation=False, incl_edges=False)
1306 ]
1308 def __init__(self, *args, **kwargs):
1309 """layer __init__ function"""
1310 super().__init__(*args, **kwargs)
1311 self.to_layerset: List[LayerSet] = []
1312 self.parent = None
1313 self.material = None
1315 @staticmethod
1316 def get_id(prefix=""):
1317 prefix_length = len(prefix)
1318 if prefix_length > 10:
1319 raise AttributeError("Max prefix length is 10!")
1320 ifcopenshell_guid = guid.new()[prefix_length + 1:]
1321 return f"{prefix}{ifcopenshell_guid}"
1323 @classmethod
1324 def pre_validate(cls, ifc) -> bool:
1325 return True
1327 def validate_creation(self) -> bool:
1328 return True
1330 def _get_thickness(self, name):
1331 """layer thickness function"""
1332 if hasattr(self.ifc, 'LayerThickness'):
1333 return self.ifc.LayerThickness * ureg.meter
1334 else:
1335 return float('nan') * ureg.meter
1337 thickness = attribute.Attribute(
1338 unit=ureg.m,
1339 functions=[_get_thickness]
1340 )
1342 is_ventilated = attribute.Attribute(
1343 description="Indication of whether the material layer represents an "
1344 "air layer (or cavity).",
1345 ifc_attr_name="IsVentilated",
1346 )
1348 description = attribute.Attribute(
1349 description="Definition of the material layer in more descriptive "
1350 "terms than given by attributes Name or Category.",
1351 ifc_attr_name="Description",
1352 )
1354 category = attribute.Attribute(
1355 description="Category of the material layer, e.g. the role it has in"
1356 " the layer set it belongs to (such as 'load bearing', "
1357 "'thermal insulation' etc.). The list of keywords might be"
1358 " extended by model view definitions, however the "
1359 "following keywords shall apply in general:",
1360 ifc_attr_name="Category",
1361 )
1363 def __repr__(self):
1364 return "<%s (material: %s>" \
1365 % (self.__class__.__name__, self.material)
1368class LayerSet(BPSProduct):
1369 """Represents a Layerset in bim2sim.
1371 Convention in bim2sim for layerset is layer 0 is inside,
1372 layer n is outside.
1374 # TODO: when not enriching we currently don't check layer orientation.
1375 """
1377 ifc_types = {
1378 "IfcMaterialLayerSet": ["*"],
1379 }
1381 guid_prefix = "LayerSet_"
1382 conditions = [
1383 condition.ListCondition('layers',
1384 critical_for_creation=False),
1385 condition.ThicknessCondition('total_thickness',
1386 threshold=0.2,
1387 critical_for_creation=False),
1388 ]
1390 def __init__(self, *args, **kwargs):
1391 """layerset __init__ function"""
1392 super().__init__(*args, **kwargs)
1393 self.parents: List[BPSProductWithLayers] = []
1394 self.layers: List[Layer] = []
1396 @staticmethod
1397 def get_id(prefix=""):
1398 prefix_length = len(prefix)
1399 if prefix_length > 10:
1400 raise AttributeError("Max prefix length is 10!")
1401 ifcopenshell_guid = guid.new()[prefix_length + 1:]
1402 return f"{prefix}{ifcopenshell_guid}"
1404 def get_total_thickness(self, name):
1405 if hasattr(self.ifc, 'TotalThickness'):
1406 if self.ifc.TotalThickness:
1407 return self.ifc.TotalThickness * ureg.m
1408 return sum(layer.thickness for layer in self.layers)
1410 def _get_volume(self, name):
1411 if hasattr(self, "net_volume"):
1412 if self.net_volume:
1413 vol = self.net_volume
1414 return vol
1415 # TODO This is not working currently, because with multiple parents
1416 # we dont know the area or width of the parent
1417 # elif self.parent.width:
1418 # vol = self.parent.volume * self.parent.width / self.thickness
1419 else:
1420 vol = float('nan') * ureg.meter ** 3
1421 # TODO see above
1422 # elif self.parent.width:
1423 # vol = self.parent.volume * self.parent.width / self.thickness
1424 else:
1425 vol = float('nan') * ureg.meter ** 3
1426 return vol
1428 thickness = attribute.Attribute(
1429 unit=ureg.m,
1430 functions=[get_total_thickness],
1431 )
1433 name = attribute.Attribute(
1434 description="The name by which the IfcMaterialLayerSet is known.",
1435 ifc_attr_name="LayerSetName",
1436 )
1438 volume = attribute.Attribute(
1439 description="Volume of layer set",
1440 functions=[_get_volume],
1441 )
1443 def __repr__(self):
1444 if self.name:
1445 return "<%s (name: %s, layers: %d)>" \
1446 % (self.__class__.__name__, self.name, len(self.layers))
1447 else:
1448 return "<%s (layers: %d)>" % (self.__class__.__name__, len(self.layers))
1451class OuterWall(Wall):
1452 ifc_types = {}
1454 def calc_cost_group(self) -> int:
1455 """Calc cost group for OuterWall
1457 Load bearing outer walls: 331
1458 Not load bearing outer walls: 332
1459 Rest: 330
1460 """
1462 if self.is_load_bearing:
1463 return 331
1464 elif not self.is_load_bearing:
1465 return 332
1466 else:
1467 return 330
1470class InnerWall(Wall):
1471 """InnerWalls are assumed to be always symmetric."""
1472 ifc_types = {}
1474 def calc_cost_group(self) -> int:
1475 """Calc cost group for InnerWall
1477 Load bearing inner walls: 341
1478 Not load bearing inner walls: 342
1479 Rest: 340
1480 """
1482 if self.is_load_bearing:
1483 return 341
1484 elif not self.is_load_bearing:
1485 return 342
1486 else:
1487 return 340
1490class Window(BPSProductWithLayers):
1491 ifc_types = {"IfcWindow": ['*', 'WINDOW', 'SKYLIGHT', 'LIGHTDOME']}
1493 pattern_ifc_type = [
1494 re.compile('Window', flags=re.IGNORECASE),
1495 re.compile('Fenster', flags=re.IGNORECASE)
1496 ]
1498 def get_glazing_area(self, name):
1499 """returns only the glazing area of the windows"""
1500 if self.glazing_ratio:
1501 return self.gross_area * self.glazing_ratio
1502 return self.opening_area
1504 def calc_cost_group(self) -> int:
1505 """Calc cost group for Windows
1507 Outer door: 334
1508 """
1510 return 334
1512 net_area = attribute.Attribute(
1513 functions=[get_glazing_area],
1514 unit=ureg.meter ** 2,
1515 )
1517 gross_area = attribute.Attribute(
1518 default_ps=("Qto_WindowBaseQuantities", "Area"),
1519 functions=[BPSProduct.get_bound_area],
1520 unit=ureg.meter ** 2
1521 )
1523 glazing_ratio = attribute.Attribute(
1524 default_ps=("Pset_WindowCommon", "GlazingAreaFraction"),
1525 )
1527 width = attribute.Attribute(
1528 default_ps=("Qto_WindowBaseQuantities", "Depth"),
1529 functions=[BPSProductWithLayers.get_thickness_by_layers],
1530 unit=ureg.m
1531 )
1532 u_value = attribute.Attribute(
1533 default_ps=("Pset_WindowCommon", "ThermalTransmittance"),
1534 unit=ureg.W / ureg.K / ureg.meter ** 2,
1535 functions=[BPSProductWithLayers.get_u_value],
1536 )
1538 g_value = attribute.Attribute( # material
1539 )
1541 a_conv = attribute.Attribute(
1542 )
1544 shading_g_total = attribute.Attribute(
1545 )
1547 shading_max_irr = attribute.Attribute(
1548 )
1550 inner_convection = attribute.Attribute(
1551 unit=ureg.W / ureg.K / ureg.meter ** 2,
1552 )
1554 inner_radiation = attribute.Attribute(
1555 unit=ureg.W / ureg.K / ureg.meter ** 2,
1556 )
1558 outer_radiation = attribute.Attribute(
1559 unit=ureg.W / ureg.K / ureg.meter ** 2,
1560 )
1562 outer_convection = attribute.Attribute(
1563 unit=ureg.W / ureg.K / ureg.meter ** 2,
1564 )
1567class Door(BPSProductWithLayers):
1568 ifc_types = {"IfcDoor": ['*', 'DOOR', 'GATE', 'TRAPDOOR']}
1570 pattern_ifc_type = [
1571 re.compile('Door', flags=re.IGNORECASE),
1572 re.compile('Tuer', flags=re.IGNORECASE)
1573 ]
1575 conditions = [
1576 condition.RangeCondition('glazing_ratio',
1577 0 * ureg.dimensionless,
1578 1 * ureg.dimensionless, True,
1579 critical_for_creation=False),
1580 ]
1582 def get_better_subclass(self):
1583 return OuterDoor if self.is_external else InnerDoor
1585 def get_net_area(self, name):
1586 if self.glazing_ratio:
1587 return self.gross_area * (1 - self.glazing_ratio)
1588 return self.gross_area - self.opening_area
1590 net_area = attribute.Attribute(
1591 functions=[get_net_area, ],
1592 unit=ureg.meter ** 2,
1593 )
1595 gross_area = attribute.Attribute(
1596 default_ps=("Qto_DoorBaseQuantities", "Area"),
1597 functions=[BPSProduct.get_bound_area],
1598 unit=ureg.meter ** 2
1599 )
1601 glazing_ratio = attribute.Attribute(
1602 default_ps=("Pset_DoorCommon", "GlazingAreaFraction"),
1603 )
1605 width = attribute.Attribute(
1606 default_ps=("Qto_DoorBaseQuantities", "Width"),
1607 functions=[BPSProductWithLayers.get_thickness_by_layers],
1608 unit=ureg.m
1609 )
1611 u_value = attribute.Attribute(
1612 unit=ureg.W / ureg.K / ureg.meter ** 2,
1613 functions=[BPSProductWithLayers.get_u_value],
1614 )
1616 inner_convection = attribute.Attribute(
1617 unit=ureg.W / ureg.K / ureg.meter ** 2,
1618 default=0.6
1619 )
1621 inner_radiation = attribute.Attribute(
1622 unit=ureg.W / ureg.K / ureg.meter ** 2,
1623 )
1625 outer_radiation = attribute.Attribute(
1626 unit=ureg.W / ureg.K / ureg.meter ** 2,
1627 )
1629 outer_convection = attribute.Attribute(
1630 unit=ureg.W / ureg.K / ureg.meter ** 2,
1631 )
1634class InnerDoor(Door):
1635 ifc_types = {}
1637 def calc_cost_group(self) -> int:
1638 """Calc cost group for Innerdoors
1640 Inner door: 344
1641 """
1643 return 344
1646class OuterDoor(Door):
1647 ifc_types = {}
1649 def calc_cost_group(self) -> int:
1650 """Calc cost group for Outerdoors
1652 Outer door: 334
1653 """
1655 return 334
1658class Slab(BPSProductWithLayers):
1659 ifc_types = {
1660 "IfcSlab": ['*', 'LANDING']
1661 }
1663 def __init__(self, *args, **kwargs):
1664 """slab __init__ function"""
1665 super().__init__(*args, **kwargs)
1667 def _calc_teaser_orientation(self, name) -> int:
1668 """Returns the orientation of the slab in TEASER convention."""
1669 return -1
1671 net_area = attribute.Attribute(
1672 default_ps=("Qto_SlabBaseQuantities", "NetArea"),
1673 functions=[BPSProduct.get_net_bound_area],
1674 unit=ureg.meter ** 2
1675 )
1677 gross_area = attribute.Attribute(
1678 default_ps=("Qto_SlabBaseQuantities", "GrossArea"),
1679 functions=[BPSProduct.get_bound_area],
1680 unit=ureg.meter ** 2
1681 )
1683 width = attribute.Attribute(
1684 default_ps=("Qto_SlabBaseQuantities", "Width"),
1685 functions=[BPSProductWithLayers.get_thickness_by_layers],
1686 unit=ureg.m
1687 )
1689 u_value = attribute.Attribute(
1690 default_ps=("Pset_SlabCommon", "ThermalTransmittance"),
1691 unit=ureg.W / ureg.K / ureg.meter ** 2,
1692 functions=[BPSProductWithLayers.get_u_value],
1693 )
1695 net_volume = attribute.Attribute(
1696 default_ps=("Qto_SlabBaseQuantities", "NetVolume"),
1697 unit=ureg.meter ** 3
1698 )
1700 is_load_bearing = attribute.Attribute(
1701 default_ps=("Pset_SlabCommon", "LoadBearing"),
1702 )
1705class Roof(Slab):
1706 # todo decomposed roofs dont have materials, layers etc. because these
1707 # information are stored in the slab itself and not the decomposition
1708 # is_external = True
1709 ifc_types = {
1710 "IfcRoof":
1711 ['*', 'FLAT_ROOF', 'SHED_ROOF', 'GABLE_ROOF', 'HIP_ROOF',
1712 'HIPPED_GABLE_ROOF', 'GAMBREL_ROOF', 'MANSARD_ROOF',
1713 'BARREL_ROOF', 'RAINBOW_ROOF', 'BUTTERFLY_ROOF', 'PAVILION_ROOF',
1714 'DOME_ROOF', 'FREEFORM'],
1715 "IfcSlab": ['ROOF']
1716 }
1718 def calc_cost_group(self) -> int:
1719 """Calc cost group for Roofs
1722 Load bearing: 361
1723 Not load bearing: 363
1724 """
1725 if self.is_load_bearing:
1726 return 361
1727 elif not self.is_load_bearing:
1728 return 363
1729 else:
1730 return 300
1733class InnerFloor(Slab):
1734 """In bim2sim we handle all inner slabs as floors/inner floors.
1736 Orientation of layerset is layer 0 is inside (floor surface of this room),
1737 layer n is outside (ceiling surface of room below).
1738 """
1739 ifc_types = {
1740 "IfcSlab": ['FLOOR']
1741 }
1743 def calc_cost_group(self) -> int:
1744 """Calc cost group for Floors
1746 Floor: 351
1747 """
1748 return 351
1751class GroundFloor(Slab):
1752 # is_external = True # todo to be removed
1753 ifc_types = {
1754 "IfcSlab": ['BASESLAB']
1755 }
1757 def _calc_teaser_orientation(self, name) -> int:
1758 """Returns the orientation of the groundfloor in TEASER convention."""
1759 return -2
1761 def calc_cost_group(self) -> int:
1762 """Calc cost group for groundfloors
1764 groundfloors: 322
1765 """
1767 return 322
1770 # pattern_ifc_type = [
1771 # re.compile('Bodenplatte', flags=re.IGNORECASE),
1772 # re.compile('')
1773 # ]
1776class Site(BPSProduct):
1777 def __init__(self, *args, **kwargs):
1778 super().__init__(*args, **kwargs)
1779 del self.building
1780 self.buildings = []
1782 # todo move this to base elements as this relevant for other domains as well
1783 ifc_types = {"IfcSite": ['*']}
1785 gross_area = attribute.Attribute(
1786 default_ps=("Qto_SiteBaseQuantities", "GrossArea"),
1787 unit=ureg.meter ** 2
1788 )
1790 location_latitude = attribute.Attribute(
1791 ifc_attr_name="RefLatitude",
1792 )
1794 location_longitude = attribute.Attribute(
1795 ifc_attr_name="RefLongitude"
1796 )
1799class Building(BPSProduct):
1800 def __init__(self, *args, **kwargs):
1801 super().__init__(*args, **kwargs)
1802 self.thermal_zones = []
1803 self.storeys = []
1804 self.elements = []
1806 ifc_types = {"IfcBuilding": ['*']}
1807 from_ifc_domains = [IFCDomain.arch]
1809 conditions = [
1810 condition.RangeCondition('year_of_construction',
1811 1900 * ureg.year,
1812 date.today().year * ureg.year,
1813 critical_for_creation=False),
1814 ]
1816 def _get_building_name(self, name):
1817 """get building name"""
1818 bldg_name = self.get_ifc_attribute('Name')
1819 if bldg_name:
1820 return bldg_name
1821 else:
1822 # todo needs to be adjusted for multiple buildings #165
1823 bldg_name = 'Building'
1824 return bldg_name
1826 def _get_number_of_storeys(self, name):
1827 return len(self.storeys)
1829 def _get_avg_storey_height(self, name):
1830 """Calculates the average height of all storeys."""
1831 storey_height_sum = 0
1832 avg_height = None
1833 if hasattr(self, "storeys"):
1834 if len(self.storeys) > 0:
1835 for storey in self.storeys:
1836 if storey.height:
1837 height = storey.height
1838 elif storey.gross_height:
1839 height = storey.gross_height
1840 elif storey.net_height:
1841 height = storey.net_height
1842 else:
1843 height = None
1844 if height:
1845 storey_height_sum += height
1846 avg_height = storey_height_sum / len(self.storeys)
1847 return avg_height
1849 def _check_tz_ahu(self, name):
1850 """Check if any TZs have AHU, then the building has one as well."""
1851 with_ahu = False
1852 for tz in self.thermal_zones:
1853 if tz.with_ahu:
1854 with_ahu = True
1855 break
1856 return with_ahu
1858 bldg_name = attribute.Attribute(
1859 functions=[_get_building_name],
1860 )
1862 year_of_construction = attribute.Attribute(
1863 default_ps=("Pset_BuildingCommon", "YearOfConstruction"),
1864 unit=ureg.year
1865 )
1867 gross_area = attribute.Attribute(
1868 default_ps=("Qto_BuildingBaseQuantities", "GrossFloorArea"),
1869 unit=ureg.meter ** 2
1870 )
1872 net_area = attribute.Attribute(
1873 default_ps=("Qto_BuildingBaseQuantities", "NetFloorArea"),
1874 unit=ureg.meter ** 2
1875 )
1877 number_of_storeys = attribute.Attribute(
1878 unit=ureg.dimensionless,
1879 functions=[_get_number_of_storeys]
1880 )
1882 occupancy_type = attribute.Attribute(
1883 default_ps=("Pset_BuildingCommon", "OccupancyType"),
1884 )
1886 avg_storey_height = attribute.Attribute(
1887 unit=ureg.meter,
1888 functions=[_get_avg_storey_height]
1889 )
1891 with_ahu = attribute.Attribute(
1892 functions=[_check_tz_ahu]
1893 )
1895 ahu_heating = attribute.Attribute(
1896 attr_type=bool
1897 )
1899 ahu_cooling = attribute.Attribute(
1900 attr_type=bool
1901 )
1903 ahu_dehumidification = attribute.Attribute(
1904 attr_type=bool
1905 )
1907 ahu_humidification = attribute.Attribute(
1908 attr_type=bool
1909 )
1911 ahu_heat_recovery = attribute.Attribute(
1912 attr_type=bool
1913 )
1915 ahu_heat_recovery_efficiency = attribute.Attribute(
1916 )
1919class Storey(BPSProduct):
1920 ifc_types = {'IfcBuildingStorey': ['*']}
1921 from_ifc_domains = [IFCDomain.arch]
1923 def __init__(self, *args, **kwargs):
1924 """storey __init__ function"""
1925 super().__init__(*args, **kwargs)
1926 self.elements = []
1928 spec_machines_internal_load = attribute.Attribute(
1929 default_ps=("Pset_ThermalLoadDesignCriteria",
1930 "ReceptacleLoadIntensity"),
1931 unit=ureg.kilowatt / (ureg.meter ** 2)
1932 )
1934 spec_lighting_internal_load = attribute.Attribute(
1935 default_ps=("Pset_ThermalLoadDesignCriteria", "LightingLoadIntensity"),
1936 unit=ureg.kilowatt / (ureg.meter ** 2)
1937 )
1939 cooling_load = attribute.Attribute(
1940 default_ps=("Pset_ThermalLoadAggregate", "TotalCoolingLoad"),
1941 unit=ureg.kilowatt
1942 )
1944 heating_load = attribute.Attribute(
1945 default_ps=("Pset_ThermalLoadAggregate", "TotalHeatingLoad"),
1946 unit=ureg.kilowatt
1947 )
1949 air_per_person = attribute.Attribute(
1950 default_ps=("Pset_ThermalLoadDesignCriteria", "OutsideAirPerPerson"),
1951 unit=ureg.meter ** 3 / ureg.hour
1952 )
1954 percent_load_to_radiant = attribute.Attribute(
1955 default_ps=("Pset_ThermalLoadDesignCriteria",
1956 "AppliancePercentLoadToRadiant"),
1957 unit=ureg.percent
1958 )
1960 gross_floor_area = attribute.Attribute(
1961 default_ps=("Qto_BuildingStoreyBaseQuantities", "GrossFloorArea"),
1962 unit=ureg.meter ** 2
1963 )
1965 # todo make the lookup for height hierarchical
1966 net_height = attribute.Attribute(
1967 default_ps=("Qto_BuildingStoreyBaseQuantities", "NetHeight"),
1968 unit=ureg.meter
1969 )
1971 gross_height = attribute.Attribute(
1972 default_ps=("Qto_BuildingStoreyBaseQuantities", "GrossHeight"),
1973 unit=ureg.meter
1974 )
1976 height = attribute.Attribute(
1977 default_ps=("Qto_BuildingStoreyBaseQuantities", "Height"),
1978 unit=ureg.meter
1979 )
1982# collect all domain classes
1983items: Set[BPSProduct] = set()
1984for name, cls in inspect.getmembers(
1985 sys.modules[__name__],
1986 lambda member: inspect.isclass(member) # class at all
1987 and issubclass(member, BPSProduct) # domain subclass
1988 and member is not BPSProduct # but not base class
1989 and member.__module__ == __name__): # declared here
1990 items.add(cls)