Coverage for bim2sim/elements/bps_elements.py: 50%
802 statements
« prev ^ index » next coverage.py v7.10.7, created at 2025-10-01 10:24 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2025-10-01 10:24 +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_Add
13from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_Transform
14from OCC.Core.BRepExtrema import BRepExtrema_DistShapeShape
15from OCC.Core.BRepGProp import brepgprop_SurfaceProperties
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.main.settings()
237 settings.set(settings.USE_PYTHON_OPENCASCADE, True)
238 settings.set(settings.USE_WORLD_COORDS, True)
239 settings.set(settings.EXCLUDE_SOLIDS_AND_SURFACES, False)
240 settings.set(settings.INCLUDE_CURVES, True)
241 return ifcopenshell.geom.create_shape(settings, self.ifc).geometry
243 def _get_space_center(self, name) -> float:
244 """
245 This function returns the center of the bounding box of an ifc space
246 shape
247 :return: center of space bounding box (gp_Pnt)
248 """
249 bbox = Bnd_Box()
250 brepbndlib_Add(self.space_shape, bbox)
251 bbox_center = ifcopenshell.geom.utils.get_bounding_box_center(bbox)
252 return bbox_center
254 def _get_footprint_shape(self, name):
255 """
256 This function returns the footprint of a space shape. This can be
257 used e.g., to visualize floor plans.
258 """
259 footprint = PyOCCTools.get_footprint_of_shape(self.space_shape)
260 return footprint
262 def _get_space_shape_volume(self, name):
263 """
264 This function returns the volume of a space shape
265 """
266 return PyOCCTools.get_shape_volume(self.space_shape)
268 def _get_volume_geometric(self, name):
269 """
270 This function returns the volume of a space geometrically
271 """
272 return self.gross_area * self.height
274 def _get_usage(self, name):
275 """
276 This function returns the usage of a space
277 """
278 if self.zone_name is not None:
279 usage = self.zone_name
280 elif self.ifc.LongName is not None and \
281 "oldSpaceGuids_" not in self.ifc.LongName:
282 # todo oldSpaceGuids_ is hardcode for erics tool
283 usage = self.ifc.LongName
284 else:
285 usage = self.name
286 return usage
288 def _get_name(self, name):
289 """
290 This function returns the name of a space
291 """
292 if self.zone_name:
293 space_name = self.zone_name
294 else:
295 space_name = self.ifc.Name
296 return space_name
298 def get_bound_floor_area(self, name):
299 """Get bound floor area of zone. This is currently set by sum of all
300 horizontal gross area and take half of it due to issues with
301 TOP BOTTOM"""
302 leveled_areas = {}
303 for height, sbs in self.horizontal_sbs.items():
304 if height not in leveled_areas:
305 leveled_areas[height] = 0
306 leveled_areas[height] += sum([sb.bound_area for sb in sbs])
308 return sum(leveled_areas.values()) / 2
310 def get_net_bound_floor_area(self, name):
311 """Get net bound floor area of zone. This is currently set by sum of all
312 horizontal net area and take half of it due to issues with TOP BOTTOM."""
313 leveled_areas = {}
314 for height, sbs in self.horizontal_sbs.items():
315 if height not in leveled_areas:
316 leveled_areas[height] = 0
317 leveled_areas[height] += sum([sb.net_bound_area for sb in sbs])
319 return sum(leveled_areas.values()) / 2
321 def _get_horizontal_sbs(self, name):
322 """get all horizonal SBs in a zone and convert them into a dict with
323 key z-height in room and the SB as value."""
324 # todo: use only bottom when TOP bottom is working correctly
325 valid = [BoundaryOrientation.top, BoundaryOrientation.bottom]
326 leveled_sbs = {}
327 for sb in self.sbs_without_corresponding:
328 if sb.top_bottom in valid:
329 pos = round(sb.position[2], 1)
330 if pos not in leveled_sbs:
331 leveled_sbs[pos] = []
332 leveled_sbs[pos].append(sb)
334 return leveled_sbs
336 def _area_specific_post_processing(self, value):
337 return value / self.net_area
339 def _get_heating_profile(self, name) -> list:
340 """returns a heating profile using the heat temperature in the IFC"""
341 # todo make this "dynamic" with a night set back
342 if self.t_set_heat is not None:
343 return [self.t_set_heat.to(ureg.kelvin).m] * 24
345 def _get_cooling_profile(self, name) -> list:
346 """returns a cooling profile using the cool temperature in the IFC"""
347 # todo make this "dynamic" with a night set back
348 if self.t_set_cool is not None:
349 return [self.t_set_cool.to(ureg.kelvin).m] * 24
351 def _get_persons(self, name):
352 if self.area_per_occupant:
353 return 1 / self.area_per_occupant
355 external_orientation = attribute.Attribute(
356 description="Orientation of the thermal zone, either 'Internal' or a "
357 "list of 2 angles or a single angle as value between 0 and "
358 "360.",
359 functions=[_get_external_orientation]
360 )
362 glass_percentage = attribute.Attribute(
363 description="Determines the glass area/facade area ratio for all the "
364 "windows in the space in one of the 4 following ranges:"
365 " 0%-30%: 15, 30%-50%: 40, 50%-70%: 60, 70%-100%: 85.",
366 functions=[_get_glass_percentage]
367 )
369 space_neighbors = attribute.Attribute(
370 description="Determines the neighbors of the thermal zone.",
371 functions=[_get_space_neighbors]
372 )
374 space_shape = attribute.Attribute(
375 description="Returns topods shape of the IfcSpace.",
376 functions=[_get_space_shape]
377 )
379 space_center = attribute.Attribute(
380 description="Returns the center of the bounding box of an ifc space "
381 "shape.",
382 functions=[_get_space_center]
383 )
385 footprint_shape = attribute.Attribute(
386 description="Returns the footprint of a space shape, which can be "
387 "used e.g., to visualize floor plans.",
388 functions=[_get_footprint_shape]
389 )
391 horizontal_sbs = attribute.Attribute(
392 description="All horizontal space boundaries in a zone as dict. Key is"
393 " the z-zeight in the room and value the SB.",
394 functions=[_get_horizontal_sbs]
395 )
397 zone_name = attribute.Attribute(
398 default_ps=("Pset_SpaceCommon", "Reference")
399 )
401 name = attribute.Attribute(
402 functions=[_get_name]
403 )
405 usage = attribute.Attribute(
406 default_ps=("Pset_SpaceOccupancyRequirements", "OccupancyType"),
407 functions=[_get_usage]
408 )
410 t_set_heat = attribute.Attribute(
411 default_ps=("Pset_SpaceThermalRequirements", "SpaceTemperatureMin"),
412 unit=ureg.degC,
413 )
415 t_set_cool = attribute.Attribute(
416 default_ps=("Pset_SpaceThermalRequirements", "SpaceTemperatureMax"),
417 unit=ureg.degC,
418 )
420 t_ground = attribute.Attribute(
421 unit=ureg.degC,
422 default=13,
423 )
425 max_humidity = attribute.Attribute(
426 default_ps=("Pset_SpaceThermalRequirements", "SpaceHumidityMax"),
427 unit=ureg.dimensionless,
428 )
430 min_humidity = attribute.Attribute(
431 default_ps=("Pset_SpaceThermalRequirements", "SpaceHumidityMin"),
432 unit=ureg.dimensionless,
433 )
435 natural_ventilation = attribute.Attribute(
436 default_ps=("Pset_SpaceThermalRequirements", "NaturalVentilation"),
437 )
439 natural_ventilation_rate = attribute.Attribute(
440 default_ps=("Pset_SpaceThermalRequirements", "NaturalVentilationRate"),
441 unit=1 / ureg.hour,
442 )
444 mechanical_ventilation_rate = attribute.Attribute(
445 default_ps=("Pset_SpaceThermalRequirements",
446 "MechanicalVentilationRate"),
447 unit=1 / ureg.hour,
448 )
450 with_ahu = attribute.Attribute(
451 default_ps=("Pset_SpaceThermalRequirements", "AirConditioning"),
452 )
454 central_ahu = attribute.Attribute(
455 default_ps=("Pset_SpaceThermalRequirements", "AirConditioningCentral"),
456 )
458 gross_area = attribute.Attribute(
459 default_ps=("Qto_SpaceBaseQuantities", "GrossFloorArea"),
460 functions=[get_bound_floor_area],
461 unit=ureg.meter ** 2
462 )
464 net_area = attribute.Attribute(
465 default_ps=("Qto_SpaceBaseQuantities", "NetFloorArea"),
466 functions=[get_net_bound_floor_area],
467 unit=ureg.meter ** 2
468 )
470 net_wall_area = attribute.Attribute(
471 default_ps=("Qto_SpaceBaseQuantities", "NetWallArea"),
472 unit=ureg.meter ** 2
473 )
475 net_ceiling_area = attribute.Attribute(
476 default_ps=("Qto_SpaceBaseQuantities", "NetCeilingArea"),
477 unit=ureg.meter ** 2
478 )
480 net_volume = attribute.Attribute(
481 default_ps=("Qto_SpaceBaseQuantities", "NetVolume"),
482 functions=[_get_space_shape_volume, _get_volume_geometric],
483 unit=ureg.meter ** 3,
484 )
485 gross_volume = attribute.Attribute(
486 default_ps=("Qto_SpaceBaseQuantities", "GrossVolume"),
487 functions=[_get_volume_geometric],
488 unit=ureg.meter ** 3,
489 )
491 height = attribute.Attribute(
492 default_ps=("Qto_SpaceBaseQuantities", "Height"),
493 unit=ureg.meter,
494 )
496 length = attribute.Attribute(
497 default_ps=("Qto_SpaceBaseQuantities", "Length"),
498 unit=ureg.meter,
499 )
501 width = attribute.Attribute(
502 default_ps=("Qto_SpaceBaseQuantities", "Width"),
503 unit=ureg.m
504 )
506 area_per_occupant = attribute.Attribute(
507 default_ps=("Pset_SpaceOccupancyRequirements", "AreaPerOccupant"),
508 unit=ureg.meter ** 2
509 )
511 space_shape_volume = attribute.Attribute(
512 functions=[_get_space_shape_volume],
513 unit=ureg.meter ** 3,
514 )
516 clothing_persons = attribute.Attribute(
517 default_ps=("", "")
518 )
520 surround_clo_persons = attribute.Attribute(
521 default_ps=("", "")
522 )
524 heating_profile = attribute.Attribute(
525 functions=[_get_heating_profile],
526 )
528 cooling_profile = attribute.Attribute(
529 functions=[_get_cooling_profile],
530 )
532 persons = attribute.Attribute(
533 functions=[_get_persons],
534 )
536 # use conditions
537 with_cooling = attribute.Attribute(
538 )
540 with_heating = attribute.Attribute(
541 )
543 T_threshold_heating = attribute.Attribute(
544 )
546 activity_degree_persons = attribute.Attribute(
547 )
549 fixed_heat_flow_rate_persons = attribute.Attribute(
550 default_ps=("Pset_SpaceThermalLoad", "People"),
551 unit=ureg.W,
552 )
554 internal_gains_moisture_no_people = attribute.Attribute(
555 )
557 T_threshold_cooling = attribute.Attribute(
558 )
560 ratio_conv_rad_persons = attribute.Attribute(
561 default=0.5,
562 )
564 ratio_conv_rad_machines = attribute.Attribute(
565 default=0.5,
566 )
568 ratio_conv_rad_lighting = attribute.Attribute(
569 default=0.5,
570 )
572 machines = attribute.Attribute(
573 description="Specific internal gains through machines, if taken from"
574 " IFC property set a division by thermal zone area is"
575 " needed.",
576 default_ps=("Pset_SpaceThermalLoad", "EquipmentSensible"),
577 ifc_postprocessing=_area_specific_post_processing,
578 unit=ureg.W / (ureg.meter ** 2),
579 )
581 def _calc_lighting_power(self, name) -> float:
582 if self.use_maintained_illuminance:
583 return self.maintained_illuminance / self.lighting_efficiency_lumen
584 else:
585 return self.fixed_lighting_power
587 lighting_power = attribute.Attribute(
588 description="Specific lighting power in W/m2. If taken from IFC"
589 " property set a division by thermal zone area is needed.",
590 default_ps=("Pset_SpaceThermalLoad", "Lighting"),
591 ifc_postprocessing=_area_specific_post_processing,
592 functions=[_calc_lighting_power],
593 unit=ureg.W / (ureg.meter ** 2),
594 )
596 fixed_lighting_power = attribute.Attribute(
597 description="Specific fixed electrical power for lighting in W/m2. "
598 "This value is taken from SIA 2024.",
599 unit=ureg.W / (ureg.meter ** 2)
600 )
602 maintained_illuminance = attribute.Attribute(
603 description="Maintained illuminance value for lighting. This value is"
604 " taken from SIA 2024.",
605 unit=ureg.lumen / (ureg.meter ** 2)
606 )
608 use_maintained_illuminance = attribute.Attribute(
609 description="Decision variable to determine if lighting_power will"
610 " be given by fixed_lighting_power or by calculation "
611 "using the variables maintained_illuminance and "
612 "lighting_efficiency_lumen. This is not available in IFC "
613 "and can be set through the sim_setting with equivalent "
614 "name. "
615 )
617 lighting_efficiency_lumen = attribute.Attribute(
618 description="Lighting efficiency in lm/W_el, in german: Lichtausbeute.",
619 unit=ureg.lumen / ureg.W
620 )
622 use_constant_infiltration = attribute.Attribute(
623 )
625 base_infiltration = attribute.Attribute(
626 )
628 max_user_infiltration = attribute.Attribute(
629 )
631 max_overheating_infiltration = attribute.Attribute(
632 )
634 max_summer_infiltration = attribute.Attribute(
635 )
637 winter_reduction_infiltration = attribute.Attribute(
638 )
640 min_ahu = attribute.Attribute(
641 )
643 max_ahu = attribute.Attribute(
644 default_ps=("Pset_AirSideSystemInformation", "TotalAirflow"),
645 unit=ureg.meter ** 3 / ureg.s
646 )
648 with_ideal_thresholds = attribute.Attribute(
649 )
651 persons_profile = attribute.Attribute(
652 )
654 machines_profile = attribute.Attribute(
655 )
657 lighting_profile = attribute.Attribute(
658 )
660 def get__elements_by_type(self, type):
661 raise NotImplementedError
663 def __repr__(self):
664 return "<%s (usage: %s)>" \
665 % (self.__class__.__name__, self.usage)
667class ExternalSpatialElement(ThermalZone):
668 ifc_types = {
669 "IfcExternalSpatialElement":
670 ['*']
671 }
674class SpaceBoundary(RelationBased):
675 ifc_types = {'IfcRelSpaceBoundary': ['*']}
677 def __init__(self, *args, elements: dict, **kwargs):
678 """spaceboundary __init__ function"""
679 super().__init__(*args, **kwargs)
680 self.disaggregation = []
681 self.bound_element = None
682 self.disagg_parent = None
683 self.bound_thermal_zone = None
684 self._elements = elements
685 self.parent_bound = None
686 self.opening_bounds = []
688 def _calc_position(self, name):
689 """
690 calculates the position of the spaceboundary, using the relative
691 position of resultant disaggregation
692 """
693 if hasattr(self.ifc.ConnectionGeometry.SurfaceOnRelatingElement,
694 'BasisSurface'):
695 position = self.ifc.ConnectionGeometry.SurfaceOnRelatingElement. \
696 BasisSurface.Position.Location.Coordinates
697 else:
698 position = self.ifc.ConnectionGeometry.SurfaceOnRelatingElement. \
699 Position.Location.Coordinates
701 return position
703 @classmethod
704 def pre_validate(cls, ifc) -> bool:
705 return True
707 def validate_creation(self) -> bool:
708 if self.bound_area and self.bound_area < 1e-2 * ureg.meter ** 2:
709 return True
710 return False
712 def get_bound_area(self, name) -> ureg.Quantity:
713 """compute area of a space boundary"""
714 bound_prop = GProp_GProps()
715 brepgprop_SurfaceProperties(self.bound_shape, bound_prop)
716 area = bound_prop.Mass()
717 return area * ureg.meter ** 2
719 bound_area = attribute.Attribute(
720 description="The area bound by the space boundary.",
721 unit=ureg.meter ** 2,
722 functions=[get_bound_area]
723 )
725 def _get_top_bottom(self, name) -> BoundaryOrientation:
726 """
727 Determines if a boundary is a top (ceiling/roof) or bottom (floor/slab)
728 element based solely on its normal vector orientation.
730 Classification is based on the dot product between the boundary's
731 normal vector and the vertical vector (0, 0, 1):
732 - TOP: when normal points upward (dot product > cos(89°))
733 - BOTTOM: when normal points downward (dot product < cos(91°))
734 - VERTICAL: when normal is perpendicular to vertical (dot product ≈ 0)
736 Returns:
737 BoundaryOrientation: Enumerated orientation classification
738 """
739 vertical_vector = gp_XYZ(0.0, 0.0, 1.0)
740 cos_angle_top = math.cos(math.radians(89))
741 cos_angle_bottom = math.cos(math.radians(91))
743 normal_dot_vertical = vertical_vector.Dot(self.bound_normal)
745 # Classify based on dot product
746 if normal_dot_vertical > cos_angle_top:
747 return BoundaryOrientation.top
748 elif normal_dot_vertical < cos_angle_bottom:
749 return BoundaryOrientation.bottom
751 return BoundaryOrientation.vertical
753 def _get_bound_center(self, name):
754 """ compute center of the bounding box of a space boundary"""
755 p = GProp_GProps()
756 brepgprop_SurfaceProperties(self.bound_shape, p)
757 return p.CentreOfMass().XYZ()
759 def _get_related_bound(self, name):
760 """
761 Get corresponding space boundary in another space,
762 ensuring that corresponding space boundaries have a matching number of
763 vertices.
764 """
765 if hasattr(self.ifc, 'CorrespondingBoundary') and \
766 self.ifc.CorrespondingBoundary is not None:
767 corr_bound = self._elements.get(
768 self.ifc.CorrespondingBoundary.GlobalId)
769 if corr_bound:
770 nb_vert_this = PyOCCTools.get_number_of_vertices(
771 self.bound_shape)
772 nb_vert_other = PyOCCTools.get_number_of_vertices(
773 corr_bound.bound_shape)
774 # if not nb_vert_this == nb_vert_other:
775 # print("NO VERT MATCH!:", nb_vert_this, nb_vert_other)
776 if nb_vert_this == nb_vert_other:
777 return corr_bound
778 else:
779 # deal with a mismatch of vertices, due to different
780 # triangulation or for other reasons. Only applicable for
781 # small differences in the bound area between the
782 # corresponding surfaces
783 if abs(self.bound_area.m - corr_bound.bound_area.m) < 0.01:
784 # get points of the current space boundary
785 p = PyOCCTools.get_points_of_face(self.bound_shape)
786 # reverse the points and create a new face. Points
787 # have to be reverted, otherwise it would result in an
788 # incorrectly oriented surface normal
789 p.reverse()
790 new_corr_shape = PyOCCTools.make_faces_from_pnts(p)
791 # move the new shape of the corresponding boundary to
792 # the original position of the corresponding boundary
793 new_moved_corr_shape = (
794 PyOCCTools.move_bounds_to_vertical_pos([
795 new_corr_shape], corr_bound.bound_shape))[0]
796 # assign the new shape to the original shape and
797 # return the new corresponding boundary
798 corr_bound.bound_shape = new_moved_corr_shape
799 return corr_bound
800 if self.bound_element is None:
801 # return None
802 # check for virtual bounds
803 if not self.physical:
804 corr_bound = None
805 # cover virtual space boundaries without related IfcVirtualElement
806 if not self.ifc.RelatedBuildingElement:
807 vbs = [b for b in self._elements.values() if
808 isinstance(b, SpaceBoundary) and not
809 b.ifc.RelatedBuildingElement]
810 for b in vbs:
811 if b is self:
812 continue
813 if b.ifc.RelatingSpace == self.ifc.RelatingSpace:
814 continue
815 if not (b.bound_area.m - self.bound_area.m) ** 2 < 1e-2:
816 continue
817 center_dist = gp_Pnt(self.bound_center).Distance(
818 gp_Pnt(b.bound_center)) ** 2
819 if center_dist > 0.5:
820 continue
821 corr_bound = b
822 return corr_bound
823 return None
824 # cover virtual space boundaries related to an IfcVirtualElement
825 if self.ifc.RelatedBuildingElement.is_a('IfcVirtualElement'):
826 if len(self.ifc.RelatedBuildingElement.ProvidesBoundaries) == 2:
827 for bound in self.ifc.RelatedBuildingElement.ProvidesBoundaries:
828 if bound.GlobalId != self.ifc.GlobalId:
829 corr_bound = self._elements[bound.GlobalId]
830 return corr_bound
831 elif len(self.bound_element.space_boundaries) == 1:
832 return None
833 elif len(self.bound_element.space_boundaries) >= 2:
834 own_space_id = self.bound_thermal_zone.ifc.GlobalId
835 min_dist = 1000
836 corr_bound = None
837 for bound in self.bound_element.space_boundaries:
838 if bound.level_description != "2a":
839 continue
840 if bound is self:
841 continue
842 # if bound.bound_normal.Dot(self.bound_normal) != -1:
843 # continue
844 other_area = bound.bound_area
845 if (other_area.m - self.bound_area.m) ** 2 > 1e-1:
846 continue
847 center_dist = gp_Pnt(self.bound_center).Distance(
848 gp_Pnt(bound.bound_center)) ** 2
849 if abs(center_dist) > 0.5:
850 continue
851 distance = BRepExtrema_DistShapeShape(
852 bound.bound_shape,
853 self.bound_shape,
854 Extrema_ExtFlag_MIN
855 ).Value()
856 if distance > min_dist:
857 continue
858 min_dist = abs(center_dist)
859 # self.check_for_vertex_duplicates(bound)
860 nb_vert_this = PyOCCTools.get_number_of_vertices(
861 self.bound_shape)
862 nb_vert_other = PyOCCTools.get_number_of_vertices(
863 bound.bound_shape)
864 # if not nb_vert_this == nb_vert_other:
865 # print("NO VERT MATCH!:", nb_vert_this, nb_vert_other)
866 if nb_vert_this == nb_vert_other:
867 corr_bound = bound
868 return corr_bound
869 else:
870 return None
872 def _get_related_adb_bound(self, name):
873 adb_bound = None
874 if self.bound_element is None:
875 return None
876 # check for visual bounds
877 if not self.physical:
878 return None
879 if self.related_bound:
880 if self.bound_thermal_zone == self.related_bound.bound_thermal_zone:
881 adb_bound = self.related_bound
882 return adb_bound
883 for bound in self.bound_element.space_boundaries:
884 if bound == self:
885 continue
886 if not bound.bound_thermal_zone == self.bound_thermal_zone:
887 continue
888 if abs(bound.bound_area.m - self.bound_area.m) > 1e-3:
889 continue
890 if all([abs(i) < 1e-3 for i in
891 ((self.bound_normal - bound.bound_normal).Coord())]):
892 continue
893 if gp_Pnt(bound.bound_center).Distance(
894 gp_Pnt(self.bound_center)) < 0.4:
895 adb_bound = bound
896 return adb_bound
898 related_adb_bound = attribute.Attribute(
899 description="Related adiabatic boundary.",
900 functions=[_get_related_adb_bound]
901 )
903 def _get_is_physical(self, name) -> bool:
904 """
905 This function returns True if the spaceboundary is physical
906 """
907 return self.ifc.PhysicalOrVirtualBoundary.lower() == 'physical'
909 def _get_bound_shape(self, name):
910 settings = ifcopenshell.geom.settings()
911 settings.set(settings.USE_PYTHON_OPENCASCADE, True)
912 settings.set(settings.USE_WORLD_COORDS, True)
913 settings.set(settings.EXCLUDE_SOLIDS_AND_SURFACES, False)
914 settings.set(settings.INCLUDE_CURVES, True)
916 # check if the space boundary shapes need a unit conversion (i.e.,
917 # an additional transformation to the correct size and position)
918 length_unit = self.ifc_units.get('IfcLengthMeasure'.lower())
919 conv_required = length_unit != ureg.meter
921 try:
922 sore = self.ifc.ConnectionGeometry.SurfaceOnRelatingElement
923 # if sore.get_info()["InnerBoundaries"] is None:
924 shape = ifcopenshell.geom.create_shape(settings, sore)
926 if sore.InnerBoundaries:
927 # shape = remove_inner_loops(shape) # todo: return None if not horizontal shape
928 # if not shape:
929 if self.bound_element.ifc.is_a(
930 'IfcWall'): # todo: remove this hotfix (generalize)
931 ifc_new = ifcopenshell.file()
932 temp_sore = ifc_new.create_entity('IfcCurveBoundedPlane',
933 OuterBoundary=sore.OuterBoundary,
934 BasisSurface=sore.BasisSurface)
935 temp_sore.InnerBoundaries = ()
936 shape = ifcopenshell.geom.create_shape(settings, temp_sore)
937 else:
938 shape = remove_inner_loops(shape)
939 if not (sore.InnerBoundaries and not self.bound_element.ifc.is_a(
940 'IfcWall')):
941 faces = PyOCCTools.get_faces_from_shape(shape)
942 if len(faces) > 1:
943 unify = ShapeUpgrade_UnifySameDomain()
944 unify.Initialize(shape)
945 unify.Build()
946 shape = unify.Shape()
947 faces = PyOCCTools.get_faces_from_shape(shape)
948 face = faces[0]
949 face = PyOCCTools.remove_coincident_and_collinear_points_from_face(
950 face)
951 shape = face
952 except:
953 try:
954 sore = self.ifc.ConnectionGeometry.SurfaceOnRelatingElement
955 ifc_new = ifcopenshell.file()
956 temp_sore = ifc_new.create_entity('IfcCurveBoundedPlane',
957 OuterBoundary=sore.OuterBoundary,
958 BasisSurface=sore.BasisSurface)
959 temp_sore.InnerBoundaries = ()
960 shape = ifcopenshell.geom.create_shape(settings, temp_sore)
961 except:
962 poly = self.ifc.ConnectionGeometry.SurfaceOnRelatingElement.OuterBoundary.Points
963 pnts = []
964 for p in poly:
965 p.Coordinates = (p.Coordinates[0], p.Coordinates[1], 0.0)
966 pnts.append((p.Coordinates[:]))
967 shape = PyOCCTools.make_faces_from_pnts(pnts)
968 shape = BRepLib_FuseEdges(shape).Shape()
970 if conv_required:
971 # scale newly created shape of space boundary to correct size
972 conv_factor = (1 * length_unit).to(
973 ureg.metre).m
974 # shape scaling seems to be covered by ifcopenshell, obsolete
975 # shape = PyOCCTools.scale_shape(shape, conv_factor, gp_Pnt(0, 0,
976 # 0))
978 if self.ifc.RelatingSpace.ObjectPlacement:
979 lp = PyOCCTools.local_placement(
980 self.ifc.RelatingSpace.ObjectPlacement).tolist()
981 # transform newly created shape of space boundary to correct
982 # position if a unit conversion is required.
983 if conv_required:
984 for i in range(len(lp)):
985 for j in range(len(lp[i])):
986 coord = lp[i][j] * length_unit
987 lp[i][j] = coord.to(ureg.meter).m
988 mat = gp_Mat(lp[0][0], lp[0][1], lp[0][2], lp[1][0], lp[1][1],
989 lp[1][2], lp[2][0], lp[2][1], lp[2][2])
990 vec = gp_Vec(lp[0][3], lp[1][3], lp[2][3])
991 trsf = gp_Trsf()
992 trsf.SetTransformation(gp_Quaternion(mat), vec)
993 shape = BRepBuilderAPI_Transform(shape, trsf).Shape()
995 # shape = shape.Reversed()
996 unify = ShapeUpgrade_UnifySameDomain()
997 unify.Initialize(shape)
998 unify.Build()
999 shape = unify.Shape()
1001 if self.bound_element is not None:
1002 bi = self.bound_element
1003 if not hasattr(bi, "related_openings"):
1004 return shape
1005 if len(bi.related_openings) == 0:
1006 return shape
1007 shape = PyOCCTools.get_face_from_shape(shape)
1008 return shape
1010 def get_level_description(self, name) -> str:
1011 """
1012 This function returns the level description of the spaceboundary
1013 """
1014 return self.ifc.Description
1016 def _get_is_external(self, name) -> Union[None, bool]:
1017 """
1018 This function returns True if the spaceboundary is external
1019 """
1020 if self.ifc.InternalOrExternalBoundary is not None:
1021 ifc_ext_internal = self.ifc.InternalOrExternalBoundary.lower()
1022 if ifc_ext_internal == 'internal':
1023 return False
1024 elif 'external' in ifc_ext_internal:
1025 return True
1026 else:
1027 return None
1028 # return not self.ifc.InternalOrExternalBoundary.lower() == 'internal'
1030 def _get_opening_area(self, name):
1031 """
1032 This function returns the opening area of the spaceboundary
1033 """
1034 if self.opening_bounds:
1035 return sum(opening_boundary.bound_area for opening_boundary
1036 in self.opening_bounds)
1037 return 0
1039 def _get_net_bound_area(self, name):
1040 """
1041 This function returns the net bound area of the spaceboundary
1042 """
1043 return self.bound_area - self.opening_area
1045 is_external = attribute.Attribute(
1046 description="True if the Space Boundary is external",
1047 functions=[_get_is_external]
1048 )
1050 bound_shape = attribute.Attribute(
1051 description="Bound shape element of the SB.",
1052 functions=[_get_bound_shape]
1053 )
1055 top_bottom = attribute.Attribute(
1056 description="Info if the SB is top "
1057 "(ceiling etc.) or bottom (floor etc.).",
1058 functions=[_get_top_bottom]
1059 )
1061 bound_center = attribute.Attribute(
1062 description="The center of the space boundary.",
1063 functions=[_get_bound_center]
1064 )
1066 related_bound = attribute.Attribute(
1067 description="Related space boundary.",
1068 functions=[_get_related_bound]
1069 )
1071 physical = attribute.Attribute(
1072 description="If the Space Boundary is physical or not.",
1073 functions=[_get_is_physical]
1074 )
1076 opening_area = attribute.Attribute(
1077 description="Opening area of the Space Boundary.",
1078 functions = [_get_opening_area]
1079 )
1081 net_bound_area = attribute.Attribute(
1082 description="Net bound area of the Space Boundary",
1083 functions=[_get_net_bound_area]
1084 )
1086 def _get_bound_normal(self, name):
1087 """
1088 This function returns the normal vector of the spaceboundary
1089 """
1090 return PyOCCTools.simple_face_normal(self.bound_shape)
1092 bound_normal = attribute.Attribute(
1093 description="Normal vector of the Space Boundary.",
1094 functions=[_get_bound_normal]
1095 )
1097 level_description = attribute.Attribute(
1098 functions=[get_level_description],
1099 # Todo this should be removed in near future. We should either
1100 # find # a way to distinguish the level of SB by something
1101 # different or should check this during the creation of SBs
1102 # and throw an error if the level is not defined.
1103 default='2a'
1104 # HACK: Rou's Model has 2a boundaries but, the description is None,
1105 # default set to 2a to temporary solve this problem
1106 )
1108 internal_external_type = attribute.Attribute(
1109 description="Defines, whether the Space Boundary is internal"
1110 " (Internal), or external, i.e. adjacent to open space "
1111 "(that can be an partially enclosed space, such as terrace"
1112 " (External",
1113 ifc_attr_name="InternalOrExternalBoundary"
1114 )
1117class ExtSpatialSpaceBoundary(SpaceBoundary):
1118 """describes all space boundaries related to an IfcExternalSpatialElement instead of an IfcSpace"""
1119 pass
1122class SpaceBoundary2B(SpaceBoundary):
1123 """describes all newly created space boundaries of type 2b to fill gaps within spaces"""
1125 def __init__(self, *args, elements=None, **kwargs):
1126 super(SpaceBoundary2B, self).__init__(*args, elements=None, **kwargs)
1127 self.ifc = ifcopenshell.create_entity('IfcRelSpaceBoundary')
1128 self.guid = None
1129 self.bound_shape = None
1130 self.thermal_zones = []
1131 self.bound_element = None
1132 self.physical = True
1133 self.is_external = False
1134 self.related_bound = None
1135 self.related_adb_bound = None
1136 self.level_description = '2b'
1139class BPSProductWithLayers(BPSProduct):
1140 ifc_types = {}
1142 def __init__(self, *args, **kwargs):
1143 """BPSProductWithLayers __init__ function.
1145 Convention in bim2sim for layerset is layer 0 is inside,
1146 layer n is outside.
1147 """
1148 super().__init__(*args, **kwargs)
1149 self.layerset = None
1151 def get_u_value(self, name):
1152 """wall get_u_value function"""
1153 layers_r = 0
1154 for layer in self.layerset.layers:
1155 if layer.thickness:
1156 if layer.material.thermal_conduc and \
1157 layer.material.thermal_conduc > 0:
1158 layers_r += layer.thickness / layer.material.thermal_conduc
1160 if layers_r > 0:
1161 return 1 / layers_r
1162 return None
1164 def get_thickness_by_layers(self, name):
1165 """calculate the total thickness of the product based on the thickness
1166 of each layer."""
1167 thickness = 0
1168 for layer in self.layerset.layers:
1169 if layer.thickness:
1170 thickness += layer.thickness
1171 return thickness
1174class Wall(BPSProductWithLayers):
1175 """Abstract wall class, only its subclasses Inner- and Outerwalls are used.
1177 Every element where self.is_external is not True, is an InnerWall.
1178 """
1179 ifc_types = {
1180 "IfcWall":
1181 ['*', 'MOVABLE', 'PARAPET', 'PARTITIONING', 'PLUMBINGWALL',
1182 'SHEAR', 'SOLIDWALL', 'POLYGONAL', 'DOOR', 'GATE', 'TRAPDOOR'],
1183 "IfcWallStandardCase":
1184 ['*', 'MOVABLE', 'PARAPET', 'PARTITIONING', 'PLUMBINGWALL',
1185 'SHEAR', 'SOLIDWALL', 'POLYGONAL', 'DOOR', 'GATE', 'TRAPDOOR'],
1186 "IfcColumn": ['*'], # Hotfix. TODO: Implement appropriate classes
1187 "IfcCurtainWall": ['*'] # Hotfix. TODO: Implement appropriate classes
1188 # "IfcElementedCase": "?" # TODO
1189 }
1191 conditions = [
1192 condition.RangeCondition('u_value',
1193 0 * ureg.W / ureg.K / ureg.meter ** 2,
1194 5 * ureg.W / ureg.K / ureg.meter ** 2,
1195 critical_for_creation=False),
1196 condition.UValueCondition('u_value',
1197 threshold=0.2,
1198 critical_for_creation=False),
1199 ]
1201 pattern_ifc_type = [
1202 re.compile('Wall', flags=re.IGNORECASE),
1203 re.compile('Wand', flags=re.IGNORECASE)
1204 ]
1206 def __init__(self, *args, **kwargs):
1207 """wall __init__ function"""
1208 super().__init__(*args, **kwargs)
1210 def get_better_subclass(self):
1211 return OuterWall if self.is_external else InnerWall
1213 net_area = attribute.Attribute(
1214 default_ps=("Qto_WallBaseQuantities", "NetSideArea"),
1215 functions=[BPSProduct.get_net_bound_area],
1216 unit=ureg.meter ** 2
1217 )
1219 gross_area = attribute.Attribute(
1220 default_ps=("Qto_WallBaseQuantities", "GrossSideArea"),
1221 functions=[BPSProduct.get_bound_area],
1222 unit=ureg.meter ** 2
1223 )
1225 tilt = attribute.Attribute(
1226 default=90
1227 )
1229 u_value = attribute.Attribute(
1230 default_ps=("Pset_WallCommon", "ThermalTransmittance"),
1231 unit=ureg.W / ureg.K / ureg.meter ** 2,
1232 functions=[BPSProductWithLayers.get_u_value],
1233 )
1235 width = attribute.Attribute(
1236 default_ps=("Qto_WallBaseQuantities", "Width"),
1237 functions=[BPSProductWithLayers.get_thickness_by_layers],
1238 unit=ureg.m
1239 )
1241 inner_convection = attribute.Attribute(
1242 unit=ureg.W / ureg.K / ureg.meter ** 2,
1243 default=0.6
1244 )
1246 is_load_bearing = attribute.Attribute(
1247 default_ps=("Pset_WallCommon", "LoadBearing"),
1248 )
1250 net_volume = attribute.Attribute(
1251 default_ps=("Qto_WallBaseQuantities", "NetVolume"),
1252 unit=ureg.meter ** 3
1253 )
1255 gross_volume = attribute.Attribute(
1256 default_ps=("Qto_WallBaseQuantities", "GrossVolume")
1257 )
1260class Layer(BPSProduct):
1261 """Represents the IfcMaterialLayer class."""
1262 ifc_types = {
1263 "IfcMaterialLayer": ["*"],
1264 }
1265 guid_prefix = "Layer_"
1267 conditions = [
1268 condition.RangeCondition('thickness',
1269 0 * ureg.m,
1270 10 * ureg.m,
1271 critical_for_creation=False, incl_edges=False)
1272 ]
1274 def __init__(self, *args, **kwargs):
1275 """layer __init__ function"""
1276 super().__init__(*args, **kwargs)
1277 self.to_layerset: List[LayerSet] = []
1278 self.parent = None
1279 self.material = None
1281 @staticmethod
1282 def get_id(prefix=""):
1283 prefix_length = len(prefix)
1284 if prefix_length > 10:
1285 raise AttributeError("Max prefix length is 10!")
1286 ifcopenshell_guid = guid.new()[prefix_length + 1:]
1287 return f"{prefix}{ifcopenshell_guid}"
1289 @classmethod
1290 def pre_validate(cls, ifc) -> bool:
1291 return True
1293 def validate_creation(self) -> bool:
1294 return True
1296 def _get_thickness(self, name):
1297 """layer thickness function"""
1298 if hasattr(self.ifc, 'LayerThickness'):
1299 return self.ifc.LayerThickness * ureg.meter
1300 else:
1301 return float('nan') * ureg.meter
1303 thickness = attribute.Attribute(
1304 unit=ureg.m,
1305 functions=[_get_thickness]
1306 )
1308 is_ventilated = attribute.Attribute(
1309 description="Indication of whether the material layer represents an "
1310 "air layer (or cavity).",
1311 ifc_attr_name="IsVentilated",
1312 )
1314 description = attribute.Attribute(
1315 description="Definition of the material layer in more descriptive "
1316 "terms than given by attributes Name or Category.",
1317 ifc_attr_name="Description",
1318 )
1320 category = attribute.Attribute(
1321 description="Category of the material layer, e.g. the role it has in"
1322 " the layer set it belongs to (such as 'load bearing', "
1323 "'thermal insulation' etc.). The list of keywords might be"
1324 " extended by model view definitions, however the "
1325 "following keywords shall apply in general:",
1326 ifc_attr_name="Category",
1327 )
1329 def __repr__(self):
1330 return "<%s (material: %s>" \
1331 % (self.__class__.__name__, self.material)
1334class LayerSet(BPSProduct):
1335 """Represents a Layerset in bim2sim.
1337 Convention in bim2sim for layerset is layer 0 is inside,
1338 layer n is outside.
1340 # TODO: when not enriching we currently don't check layer orientation.
1341 """
1343 ifc_types = {
1344 "IfcMaterialLayerSet": ["*"],
1345 }
1347 guid_prefix = "LayerSet_"
1348 conditions = [
1349 condition.ListCondition('layers',
1350 critical_for_creation=False),
1351 condition.ThicknessCondition('total_thickness',
1352 threshold=0.2,
1353 critical_for_creation=False),
1354 ]
1356 def __init__(self, *args, **kwargs):
1357 """layerset __init__ function"""
1358 super().__init__(*args, **kwargs)
1359 self.parents: List[BPSProductWithLayers] = []
1360 self.layers: List[Layer] = []
1362 @staticmethod
1363 def get_id(prefix=""):
1364 prefix_length = len(prefix)
1365 if prefix_length > 10:
1366 raise AttributeError("Max prefix length is 10!")
1367 ifcopenshell_guid = guid.new()[prefix_length + 1:]
1368 return f"{prefix}{ifcopenshell_guid}"
1370 def get_total_thickness(self, name):
1371 if hasattr(self.ifc, 'TotalThickness'):
1372 if self.ifc.TotalThickness:
1373 return self.ifc.TotalThickness * ureg.m
1374 return sum(layer.thickness for layer in self.layers)
1376 def _get_volume(self, name):
1377 if hasattr(self, "net_volume"):
1378 if self.net_volume:
1379 vol = self.net_volume
1380 return vol
1381 # TODO This is not working currently, because with multiple parents
1382 # we dont know the area or width of the parent
1383 # elif self.parent.width:
1384 # vol = self.parent.volume * self.parent.width / self.thickness
1385 else:
1386 vol = float('nan') * ureg.meter ** 3
1387 # TODO see above
1388 # elif self.parent.width:
1389 # vol = self.parent.volume * self.parent.width / self.thickness
1390 else:
1391 vol = float('nan') * ureg.meter ** 3
1392 return vol
1394 thickness = attribute.Attribute(
1395 unit=ureg.m,
1396 functions=[get_total_thickness],
1397 )
1399 name = attribute.Attribute(
1400 description="The name by which the IfcMaterialLayerSet is known.",
1401 ifc_attr_name="LayerSetName",
1402 )
1404 volume = attribute.Attribute(
1405 description="Volume of layer set",
1406 functions=[_get_volume],
1407 )
1409 def __repr__(self):
1410 if self.name:
1411 return "<%s (name: %s, layers: %d)>" \
1412 % (self.__class__.__name__, self.name, len(self.layers))
1413 else:
1414 return "<%s (layers: %d)>" % (self.__class__.__name__, len(self.layers))
1417class OuterWall(Wall):
1418 ifc_types = {}
1420 def calc_cost_group(self) -> int:
1421 """Calc cost group for OuterWall
1423 Load bearing outer walls: 331
1424 Not load bearing outer walls: 332
1425 Rest: 330
1426 """
1428 if self.is_load_bearing:
1429 return 331
1430 elif not self.is_load_bearing:
1431 return 332
1432 else:
1433 return 330
1436class InnerWall(Wall):
1437 """InnerWalls are assumed to be always symmetric."""
1438 ifc_types = {}
1440 def calc_cost_group(self) -> int:
1441 """Calc cost group for InnerWall
1443 Load bearing inner walls: 341
1444 Not load bearing inner walls: 342
1445 Rest: 340
1446 """
1448 if self.is_load_bearing:
1449 return 341
1450 elif not self.is_load_bearing:
1451 return 342
1452 else:
1453 return 340
1456class Window(BPSProductWithLayers):
1457 ifc_types = {"IfcWindow": ['*', 'WINDOW', 'SKYLIGHT', 'LIGHTDOME']}
1459 pattern_ifc_type = [
1460 re.compile('Window', flags=re.IGNORECASE),
1461 re.compile('Fenster', flags=re.IGNORECASE)
1462 ]
1464 def get_glazing_area(self, name):
1465 """returns only the glazing area of the windows"""
1466 if self.glazing_ratio:
1467 return self.gross_area * self.glazing_ratio
1468 return self.opening_area
1470 def calc_cost_group(self) -> int:
1471 """Calc cost group for Windows
1473 Outer door: 334
1474 """
1476 return 334
1478 net_area = attribute.Attribute(
1479 functions=[get_glazing_area],
1480 unit=ureg.meter ** 2,
1481 )
1483 gross_area = attribute.Attribute(
1484 default_ps=("Qto_WindowBaseQuantities", "Area"),
1485 functions=[BPSProduct.get_bound_area],
1486 unit=ureg.meter ** 2
1487 )
1489 glazing_ratio = attribute.Attribute(
1490 default_ps=("Pset_WindowCommon", "GlazingAreaFraction"),
1491 )
1493 width = attribute.Attribute(
1494 default_ps=("Qto_WindowBaseQuantities", "Depth"),
1495 functions=[BPSProductWithLayers.get_thickness_by_layers],
1496 unit=ureg.m
1497 )
1498 u_value = attribute.Attribute(
1499 default_ps=("Pset_WindowCommon", "ThermalTransmittance"),
1500 unit=ureg.W / ureg.K / ureg.meter ** 2,
1501 functions=[BPSProductWithLayers.get_u_value],
1502 )
1504 g_value = attribute.Attribute( # material
1505 )
1507 a_conv = attribute.Attribute(
1508 )
1510 shading_g_total = attribute.Attribute(
1511 )
1513 shading_max_irr = attribute.Attribute(
1514 )
1516 inner_convection = attribute.Attribute(
1517 unit=ureg.W / ureg.K / ureg.meter ** 2,
1518 )
1520 inner_radiation = attribute.Attribute(
1521 unit=ureg.W / ureg.K / ureg.meter ** 2,
1522 )
1524 outer_radiation = attribute.Attribute(
1525 unit=ureg.W / ureg.K / ureg.meter ** 2,
1526 )
1528 outer_convection = attribute.Attribute(
1529 unit=ureg.W / ureg.K / ureg.meter ** 2,
1530 )
1533class Door(BPSProductWithLayers):
1534 ifc_types = {"IfcDoor": ['*', 'DOOR', 'GATE', 'TRAPDOOR']}
1536 pattern_ifc_type = [
1537 re.compile('Door', flags=re.IGNORECASE),
1538 re.compile('Tuer', flags=re.IGNORECASE)
1539 ]
1541 conditions = [
1542 condition.RangeCondition('glazing_ratio',
1543 0 * ureg.dimensionless,
1544 1 * ureg.dimensionless, True,
1545 critical_for_creation=False),
1546 ]
1548 def get_better_subclass(self):
1549 return OuterDoor if self.is_external else InnerDoor
1551 def get_net_area(self, name):
1552 if self.glazing_ratio:
1553 return self.gross_area * (1 - self.glazing_ratio)
1554 return self.gross_area - self.opening_area
1556 net_area = attribute.Attribute(
1557 functions=[get_net_area, ],
1558 unit=ureg.meter ** 2,
1559 )
1561 gross_area = attribute.Attribute(
1562 default_ps=("Qto_DoorBaseQuantities", "Area"),
1563 functions=[BPSProduct.get_bound_area],
1564 unit=ureg.meter ** 2
1565 )
1567 glazing_ratio = attribute.Attribute(
1568 default_ps=("Pset_DoorCommon", "GlazingAreaFraction"),
1569 )
1571 width = attribute.Attribute(
1572 default_ps=("Qto_DoorBaseQuantities", "Width"),
1573 functions=[BPSProductWithLayers.get_thickness_by_layers],
1574 unit=ureg.m
1575 )
1577 u_value = attribute.Attribute(
1578 unit=ureg.W / ureg.K / ureg.meter ** 2,
1579 functions=[BPSProductWithLayers.get_u_value],
1580 )
1582 inner_convection = attribute.Attribute(
1583 unit=ureg.W / ureg.K / ureg.meter ** 2,
1584 default=0.6
1585 )
1587 inner_radiation = attribute.Attribute(
1588 unit=ureg.W / ureg.K / ureg.meter ** 2,
1589 )
1591 outer_radiation = attribute.Attribute(
1592 unit=ureg.W / ureg.K / ureg.meter ** 2,
1593 )
1595 outer_convection = attribute.Attribute(
1596 unit=ureg.W / ureg.K / ureg.meter ** 2,
1597 )
1600class InnerDoor(Door):
1601 ifc_types = {}
1603 def calc_cost_group(self) -> int:
1604 """Calc cost group for Innerdoors
1606 Inner door: 344
1607 """
1609 return 344
1612class OuterDoor(Door):
1613 ifc_types = {}
1615 def calc_cost_group(self) -> int:
1616 """Calc cost group for Outerdoors
1618 Outer door: 334
1619 """
1621 return 334
1624class Slab(BPSProductWithLayers):
1625 ifc_types = {
1626 "IfcSlab": ['*', 'LANDING']
1627 }
1629 def __init__(self, *args, **kwargs):
1630 """slab __init__ function"""
1631 super().__init__(*args, **kwargs)
1633 def _calc_teaser_orientation(self, name) -> int:
1634 """Returns the orientation of the slab in TEASER convention."""
1635 return -1
1637 net_area = attribute.Attribute(
1638 default_ps=("Qto_SlabBaseQuantities", "NetArea"),
1639 functions=[BPSProduct.get_net_bound_area],
1640 unit=ureg.meter ** 2
1641 )
1643 gross_area = attribute.Attribute(
1644 default_ps=("Qto_SlabBaseQuantities", "GrossArea"),
1645 functions=[BPSProduct.get_bound_area],
1646 unit=ureg.meter ** 2
1647 )
1649 width = attribute.Attribute(
1650 default_ps=("Qto_SlabBaseQuantities", "Width"),
1651 functions=[BPSProductWithLayers.get_thickness_by_layers],
1652 unit=ureg.m
1653 )
1655 u_value = attribute.Attribute(
1656 default_ps=("Pset_SlabCommon", "ThermalTransmittance"),
1657 unit=ureg.W / ureg.K / ureg.meter ** 2,
1658 functions=[BPSProductWithLayers.get_u_value],
1659 )
1661 net_volume = attribute.Attribute(
1662 default_ps=("Qto_SlabBaseQuantities", "NetVolume"),
1663 unit=ureg.meter ** 3
1664 )
1666 is_load_bearing = attribute.Attribute(
1667 default_ps=("Pset_SlabCommon", "LoadBearing"),
1668 )
1671class Roof(Slab):
1672 # todo decomposed roofs dont have materials, layers etc. because these
1673 # information are stored in the slab itself and not the decomposition
1674 # is_external = True
1675 ifc_types = {
1676 "IfcRoof":
1677 ['*', 'FLAT_ROOF', 'SHED_ROOF', 'GABLE_ROOF', 'HIP_ROOF',
1678 'HIPPED_GABLE_ROOF', 'GAMBREL_ROOF', 'MANSARD_ROOF',
1679 'BARREL_ROOF', 'RAINBOW_ROOF', 'BUTTERFLY_ROOF', 'PAVILION_ROOF',
1680 'DOME_ROOF', 'FREEFORM'],
1681 "IfcSlab": ['ROOF']
1682 }
1684 def calc_cost_group(self) -> int:
1685 """Calc cost group for Roofs
1688 Load bearing: 361
1689 Not load bearing: 363
1690 """
1691 if self.is_load_bearing:
1692 return 361
1693 elif not self.is_load_bearing:
1694 return 363
1695 else:
1696 return 300
1699class InnerFloor(Slab):
1700 """In bim2sim we handle all inner slabs as floors/inner floors.
1702 Orientation of layerset is layer 0 is inside (floor surface of this room),
1703 layer n is outside (ceiling surface of room below).
1704 """
1705 ifc_types = {
1706 "IfcSlab": ['FLOOR']
1707 }
1709 def calc_cost_group(self) -> int:
1710 """Calc cost group for Floors
1712 Floor: 351
1713 """
1714 return 351
1717class GroundFloor(Slab):
1718 # is_external = True # todo to be removed
1719 ifc_types = {
1720 "IfcSlab": ['BASESLAB']
1721 }
1723 def _calc_teaser_orientation(self, name) -> int:
1724 """Returns the orientation of the groundfloor in TEASER convention."""
1725 return -2
1727 def calc_cost_group(self) -> int:
1728 """Calc cost group for groundfloors
1730 groundfloors: 322
1731 """
1733 return 322
1736 # pattern_ifc_type = [
1737 # re.compile('Bodenplatte', flags=re.IGNORECASE),
1738 # re.compile('')
1739 # ]
1742class Site(BPSProduct):
1743 def __init__(self, *args, **kwargs):
1744 super().__init__(*args, **kwargs)
1745 del self.building
1746 self.buildings = []
1748 # todo move this to base elements as this relevant for other domains as well
1749 ifc_types = {"IfcSite": ['*']}
1751 gross_area = attribute.Attribute(
1752 default_ps=("Qto_SiteBaseQuantities", "GrossArea"),
1753 unit=ureg.meter ** 2
1754 )
1756 location_latitude = attribute.Attribute(
1757 ifc_attr_name="RefLatitude",
1758 )
1760 location_longitude = attribute.Attribute(
1761 ifc_attr_name="RefLongitude"
1762 )
1765class Building(BPSProduct):
1766 def __init__(self, *args, **kwargs):
1767 super().__init__(*args, **kwargs)
1768 self.thermal_zones = []
1769 self.storeys = []
1770 self.elements = []
1772 ifc_types = {"IfcBuilding": ['*']}
1773 from_ifc_domains = [IFCDomain.arch]
1775 conditions = [
1776 condition.RangeCondition('year_of_construction',
1777 1900 * ureg.year,
1778 date.today().year * ureg.year,
1779 critical_for_creation=False),
1780 ]
1782 def _get_building_name(self, name):
1783 """get building name"""
1784 bldg_name = self.get_ifc_attribute('Name')
1785 if bldg_name:
1786 return bldg_name
1787 else:
1788 # todo needs to be adjusted for multiple buildings #165
1789 bldg_name = 'Building'
1790 return bldg_name
1792 def _get_number_of_storeys(self, name):
1793 return len(self.storeys)
1795 def _get_avg_storey_height(self, name):
1796 """Calculates the average height of all storeys."""
1797 storey_height_sum = 0
1798 avg_height = None
1799 if hasattr(self, "storeys"):
1800 if len(self.storeys) > 0:
1801 for storey in self.storeys:
1802 if storey.height:
1803 height = storey.height
1804 elif storey.gross_height:
1805 height = storey.gross_height
1806 elif storey.net_height:
1807 height = storey.net_height
1808 else:
1809 height = None
1810 if height:
1811 storey_height_sum += height
1812 avg_height = storey_height_sum / len(self.storeys)
1813 return avg_height
1815 def _check_tz_ahu(self, name):
1816 """Check if any TZs have AHU, then the building has one as well."""
1817 with_ahu = False
1818 for tz in self.thermal_zones:
1819 if tz.with_ahu:
1820 with_ahu = True
1821 break
1822 return with_ahu
1824 bldg_name = attribute.Attribute(
1825 functions=[_get_building_name],
1826 )
1828 year_of_construction = attribute.Attribute(
1829 default_ps=("Pset_BuildingCommon", "YearOfConstruction"),
1830 unit=ureg.year
1831 )
1833 gross_area = attribute.Attribute(
1834 default_ps=("Qto_BuildingBaseQuantities", "GrossFloorArea"),
1835 unit=ureg.meter ** 2
1836 )
1838 net_area = attribute.Attribute(
1839 default_ps=("Qto_BuildingBaseQuantities", "NetFloorArea"),
1840 unit=ureg.meter ** 2
1841 )
1843 number_of_storeys = attribute.Attribute(
1844 unit=ureg.dimensionless,
1845 functions=[_get_number_of_storeys]
1846 )
1848 occupancy_type = attribute.Attribute(
1849 default_ps=("Pset_BuildingCommon", "OccupancyType"),
1850 )
1852 avg_storey_height = attribute.Attribute(
1853 unit=ureg.meter,
1854 functions=[_get_avg_storey_height]
1855 )
1857 with_ahu = attribute.Attribute(
1858 functions=[_check_tz_ahu]
1859 )
1861 ahu_heating = attribute.Attribute(
1862 attr_type=bool
1863 )
1865 ahu_cooling = attribute.Attribute(
1866 attr_type=bool
1867 )
1869 ahu_dehumidification = attribute.Attribute(
1870 attr_type=bool
1871 )
1873 ahu_humidification = attribute.Attribute(
1874 attr_type=bool
1875 )
1877 ahu_heat_recovery = attribute.Attribute(
1878 attr_type=bool
1879 )
1881 ahu_heat_recovery_efficiency = attribute.Attribute(
1882 )
1885class Storey(BPSProduct):
1886 ifc_types = {'IfcBuildingStorey': ['*']}
1887 from_ifc_domains = [IFCDomain.arch]
1889 def __init__(self, *args, **kwargs):
1890 """storey __init__ function"""
1891 super().__init__(*args, **kwargs)
1892 self.elements = []
1894 spec_machines_internal_load = attribute.Attribute(
1895 default_ps=("Pset_ThermalLoadDesignCriteria",
1896 "ReceptacleLoadIntensity"),
1897 unit=ureg.kilowatt / (ureg.meter ** 2)
1898 )
1900 spec_lighting_internal_load = attribute.Attribute(
1901 default_ps=("Pset_ThermalLoadDesignCriteria", "LightingLoadIntensity"),
1902 unit=ureg.kilowatt / (ureg.meter ** 2)
1903 )
1905 cooling_load = attribute.Attribute(
1906 default_ps=("Pset_ThermalLoadAggregate", "TotalCoolingLoad"),
1907 unit=ureg.kilowatt
1908 )
1910 heating_load = attribute.Attribute(
1911 default_ps=("Pset_ThermalLoadAggregate", "TotalHeatingLoad"),
1912 unit=ureg.kilowatt
1913 )
1915 air_per_person = attribute.Attribute(
1916 default_ps=("Pset_ThermalLoadDesignCriteria", "OutsideAirPerPerson"),
1917 unit=ureg.meter ** 3 / ureg.hour
1918 )
1920 percent_load_to_radiant = attribute.Attribute(
1921 default_ps=("Pset_ThermalLoadDesignCriteria",
1922 "AppliancePercentLoadToRadiant"),
1923 unit=ureg.percent
1924 )
1926 gross_floor_area = attribute.Attribute(
1927 default_ps=("Qto_BuildingStoreyBaseQuantities", "GrossFloorArea"),
1928 unit=ureg.meter ** 2
1929 )
1931 # todo make the lookup for height hierarchical
1932 net_height = attribute.Attribute(
1933 default_ps=("Qto_BuildingStoreyBaseQuantities", "NetHeight"),
1934 unit=ureg.meter
1935 )
1937 gross_height = attribute.Attribute(
1938 default_ps=("Qto_BuildingStoreyBaseQuantities", "GrossHeight"),
1939 unit=ureg.meter
1940 )
1942 height = attribute.Attribute(
1943 default_ps=("Qto_BuildingStoreyBaseQuantities", "Height"),
1944 unit=ureg.meter
1945 )
1948# collect all domain classes
1949items: Set[BPSProduct] = set()
1950for name, cls in inspect.getmembers(
1951 sys.modules[__name__],
1952 lambda member: inspect.isclass(member) # class at all
1953 and issubclass(member, BPSProduct) # domain subclass
1954 and member is not BPSProduct # but not base class
1955 and member.__module__ == __name__): # declared here
1956 items.add(cls)