Coverage for bim2sim/tasks/bps/sb_2b_generation.py: 22%
98 statements
« prev ^ index » next coverage.py v7.6.12, created at 2025-03-12 17:09 +0000
« prev ^ index » next coverage.py v7.6.12, created at 2025-03-12 17:09 +0000
1import logging
3import ifcopenshell
4from ifcopenshell import guid
5from OCC.Core.BRepAlgoAPI import BRepAlgoAPI_Cut
6from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_MakeVertex
7from OCC.Core.BRepExtrema import BRepExtrema_DistShapeShape
8from OCC.Core.Extrema import Extrema_ExtFlag_MIN
9from OCC.Core.TopoDS import TopoDS_Face
10from OCC.Core.gp import gp_Pnt
12from bim2sim.elements.bps_elements import SpaceBoundary2B, ThermalZone, Door, \
13 Window
14from bim2sim.tasks.base import ITask
15from bim2sim.tasks.bps import CorrectSpaceBoundaries
16from bim2sim.utilities.common_functions import get_spaces_with_bounds
17from bim2sim.utilities.pyocc_tools import PyOCCTools
19logger = logging.getLogger(__name__)
22class AddSpaceBoundaries2B(ITask):
23 """Fill gaps in set of space boundary per space with 2B space boundaries."""
25 reads = ('elements',)
26 touches = ('elements',)
28 def run(self, elements: dict) -> tuple[dict]:
29 """Create 2b space boundaries to fill gaps in spaces.
31 This task generates space boundaries of type 2b to fill gaps in the
32 space surrounding space boundaries. The resulting set of space
33 boundaries forms a watertight shape.
35 Args:
36 elements (dict): dictionary in the format dict[guid: element],
37 holds preprocessed elements including space boundaries.
38 Returns:
39 elements (dict): dictionary in the format dict[guid: element],
40 holds preprocessed elements including space boundaries and
41 generated 2b space boundaries.
42 """
43 if not self.playground.sim_settings.close_space_boundary_gaps:
44 return elements,
45 try:
46 total_area_before_split = 0
47 inst_2b = self._compute_2b_bound_gaps(elements)
48 for sb in inst_2b.values():
49 total_area_before_split += sb.bound_area
50 CorrectSpaceBoundaries.split_non_convex_bounds(
51 CorrectSpaceBoundaries(self.playground),
52 inst_2b,
53 self.playground.sim_settings.split_bounds)
54 total_area_after_split = 0
55 for sb in inst_2b.values():
56 total_area_after_split += sb.bound_area
57 rel_dif_per = (abs(total_area_before_split-total_area_after_split)/
58 total_area_before_split * 100)
59 if rel_dif_per > 1e-3:
60 self.logger.warning(
61 f"Difference of {rel_dif_per.m} between total 2B area "
62 f"before and after splitting.")
63 except Exception as ex:
64 logger.warning(f"Unexpected {ex=}. No 2b Space Boundaries added."
65 f" {type(ex)=}")
66 return elements,
67 elements.update(inst_2b)
69 return elements,
71 def _compute_2b_bound_gaps(self, elements: dict)\
72 -> dict[str:SpaceBoundary2B]:
73 """Compute 2b space boundaries for gaps between 2a space boundaries.
75 This function computes 2b space boundaries for non-watertight sets of
76 space boundaries of type 2a. The 2b generation algorithm cuts all
77 individual existing space boundary shapes from the space shape. The
78 resulting surfaces close the gaps in the space boundary shapes. These
79 new shapes are assigned to be shapes of type 2b for simplicity. The
80 algorithm could be further improved by a verification of the space
81 boundary type (type 2a or 2b).
83 Args:
84 elements: dict[guid: element]
86 Returns:
87 dict[guid: SpaceBoundary2B]
88 """
89 logger.info("Generate space boundaries of type 2B")
90 inst_2b = dict()
91 spaces = get_spaces_with_bounds(elements)
92 for space_obj in spaces:
93 # compare surface area of IfcSpace shape with sum of space
94 # boundary shapes of this thermal zone.
95 space_surf_area = PyOCCTools.get_shape_area(space_obj.space_shape)
96 sb_area = 0
97 for bound in space_obj.space_boundaries:
98 if bound.parent_bound:
99 continue
100 sb_area += PyOCCTools.get_shape_area(bound.bound_shape)
101 if (space_surf_area - sb_area) < 1e-2:
102 continue
103 # spaces which reach this point have gaps in their boundaries.
104 space_obj.b_bound_shape = space_obj.space_shape
105 for bound in space_obj.space_boundaries:
106 if bound.bound_area.m == 0:
107 continue
108 if PyOCCTools.get_shape_area(space_obj.b_bound_shape) == 0:
109 continue
110 # exclude surfaces that are too far from space shape.
111 distance = BRepExtrema_DistShapeShape(
112 space_obj.b_bound_shape,
113 bound.bound_shape,
114 Extrema_ExtFlag_MIN).Value()
115 if distance > 1e-6:
116 continue
117 # cut the current shape from the (leftover) space shape.
118 space_obj.b_bound_shape = BRepAlgoAPI_Cut(
119 space_obj.b_bound_shape, bound.bound_shape).Shape()
120 # extract faces from the leftover shape.
121 faces = PyOCCTools.get_faces_from_shape(space_obj.b_bound_shape)
122 if faces:
123 # create a new 2b space boundary for each face..
124 inst_2b.update(self.create_2b_space_boundaries(faces, space_obj))
125 return inst_2b
127 @staticmethod
128 def create_2b_space_boundaries(faces: list[TopoDS_Face],
129 space_obj: ThermalZone)\
130 -> dict[str: SpaceBoundary2B]:
131 """Create new 2b space boundaries.
133 Create a new 2b space boundary for each face in the list and
134 assign it to the space instance.
136 Args:
137 faces: list of TopoDS_Face
138 space_obj: ThermalZone instance
140 Returns:
141 dict[guid: SpaceBoundary2B]
143 """
144 settings = ifcopenshell.geom.main.settings()
145 settings.set(settings.USE_PYTHON_OPENCASCADE, True)
146 settings.set(settings.USE_WORLD_COORDS, True)
147 settings.set(settings.EXCLUDE_SOLIDS_AND_SURFACES, False)
148 settings.set(settings.INCLUDE_CURVES, True)
149 inst_2b = dict()
150 bound_obj = []
152 # generate a list of IFCBased elements (e.g. Wall) that are the
153 # space surrounding elements. Initialize a shape (geometry) for these
154 # elements.
155 for bound in space_obj.space_boundaries:
156 if bound.bound_element and bound.bound_element.ifc.Representation:
157 bi = bound.bound_element.ifc
158 bound.bound_element.shape = ifcopenshell.geom.create_shape(
159 settings, bi).geometry
160 bound_obj.append(bound.bound_element)
162 for i, face in enumerate(faces):
163 b_bound = SpaceBoundary2B()
164 b_bound.bound_shape = face
165 if b_bound.bound_area.m < 1e-3:
166 continue
167 b_bound.guid = guid.new()
168 b_bound.bound_thermal_zone = space_obj
169 # get the building element that is bounded by the current 2b bound
170 for instance in bound_obj:
171 if isinstance(instance, Door) or isinstance(instance, Window):
172 continue
173 center_shape = BRepBuilderAPI_MakeVertex(
174 gp_Pnt(b_bound.bound_center)).Shape()
175 distance = BRepExtrema_DistShapeShape(
176 center_shape, instance.shape, Extrema_ExtFlag_MIN).Value()
177 if distance < 1e-3:
178 b_bound.bound_element = instance
179 break
180 space_obj.space_boundaries.append(b_bound)
181 b_bound.bound_element.space_boundaries.append(b_bound)
182 inst_2b[b_bound.guid] = b_bound
183 return inst_2b