Coverage for bim2sim/plugins/PluginEnergyPlus/bim2sim_energyplus/utils/utils_visualization.py: 0%
161 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 colorsys
2from pathlib import Path
3from typing import List
5import ifcopenshell
6import ifcopenshell.geom
7import pandas as pd
8from OCC.Core.Quantity import Quantity_Color, Quantity_TOC_RGB
9from OCC.Core.TopoDS import TopoDS_Shape
10from OCC.Core.gp import gp_Pnt
11from OCC.Display.SimpleGui import init_display
12from PIL import Image, ImageFont, ImageDraw
14from bim2sim.plugins.PluginEnergyPlus.bim2sim_energyplus.utils import \
15 PostprocessingUtils
16from bim2sim.utilities.pyocc_tools import PyOCCTools
19class VisualizationUtils:
21 @staticmethod
22 def _display_shape_of_space_boundaries(elements):
23 """Display topoDS_shapes of space boundaries"""
24 display, start_display, add_menu, add_function_to_menu = init_display()
25 colors = ['blue', 'red', 'magenta', 'yellow', 'green', 'white', 'cyan']
26 col = 0
27 for inst in elements:
28 if elements[inst].ifc.is_a('IfcRelSpaceBoundary'):
29 col += 1
30 bound = elements[inst]
31 if bound.bound_element is None:
32 continue
33 if not bound.bound_element.ifc.is_a("IfcWall"):
34 pass
35 try:
36 display.DisplayShape(bound.bound_shape, color=colors[(col - 1) % len(colors)])
37 except:
38 continue
39 display.FitAll()
40 start_display()
42 @staticmethod
43 def _display_bound_normal_orientation(elements):
44 display, start_display, add_menu, add_function_to_menu = init_display()
45 col = 0
46 for inst in elements:
47 if not elements[inst].ifc.is_a('IfcSpace'):
48 continue
49 space = elements[inst]
50 for bound in space.space_boundaries:
51 face_towards_center = space.space_center.XYZ() - \
52 bound.bound_center
53 face_towards_center.Normalize()
54 dot = face_towards_center.Dot(bound.bound_normal)
55 if dot > 0:
56 display.DisplayShape(bound.bound_shape, color="red")
57 else:
58 display.DisplayShape(bound.bound_shape, color="green")
59 display.FitAll()
60 start_display()
62 @staticmethod
63 def display_occ_shapes(shapes: List[TopoDS_Shape]):
64 """Display topoDS_shapes of space boundaries"""
65 display, start_display, add_menu, add_function_to_menu = init_display()
66 for shape in shapes:
67 try:
68 display.DisplayShape(shape)
69 except:
70 continue
71 display.FitAll()
72 start_display()
74 @staticmethod
75 def rgb_color(rgb) -> Quantity_Color:
76 """Returns a OCC viewer compatible color quantity based on r,g,
77 b values.
78 Args:
79 rgb: must be a tuple with 3 values [0,1]. e.g. (0, 0.5, 0.7)
80 Returns:
81 Quantity_Color object which is compatible with with the OCC viewer.
82 """
83 return Quantity_Color(rgb[0], rgb[1], rgb[2], Quantity_TOC_RGB)
85 @staticmethod
86 def rgb(minimum, maximum, value):
87 minimum, maximum = float(minimum), float(maximum)
88 ratio = 2 * (value - minimum) / (maximum - minimum)
89 r = int(max(0, 255 * (1 - ratio))) / 255
90 b = int(max(0, abs(255 * (ratio - 1)))) / 255
91 g = 1
92 return (r, g, b)
94 @staticmethod
95 def interpolate_to_rgb(minimum, maximum, value, color_min=50,
96 color_max=340):
97 s = 1
98 l = 0.5
99 h = (color_min + (color_max - color_min) / (maximum - minimum) * (
100 value - minimum)) / 360
101 r, g, b = colorsys.hls_to_rgb(h, l, s)
103 return r, g, b
105 @staticmethod
106 def get_column_from_ep_results(csv_name: str, column_key: str) -> \
107 pd.DataFrame:
108 """
109 This function extracts a column per energyplus column_key from a
110 dataframe. It removes the full column_key from the column name
111 afterwards, such that only the zone identifier (guid) of the column
112 name remains as column key.
114 Args:
115 csv_name: csv name including file name as string
116 column_key: full variable key that is removed afterwards from
117 column name. Must not include the zone guid
118 Returns:
119 df: pandas dataframe
120 """
121 res_df = pd.read_csv(csv_name)
122 # extract data that includes the column_key
123 df = PostprocessingUtils._extract_cols_from_df(
124 res_df, column_key)
125 # rename column name to zone guid (remove column_key from column
126 # name and keep guid only)
127 df.columns = [
128 col.replace(column_key, '') for col in
129 df.columns]
130 return df
132 @staticmethod
133 def add_legend(save_path, paths, minimum, maximum, unit,
134 text_size=25, text_color=(0, 0, 0,), legend_height=800,
135 title=None):
136 # add legend to 3D image
137 im = Image.open(save_path)
138 draw = ImageDraw.Draw(im)
139 #font_path = folder_structure.assets / 'fonts' / 'arial.ttf'
140 #title_font = ImageFont.truetype(str(font_path), text_size)
141 blind_counter = 0
142 im_width, im_heigth = im.size
143 line_width = int(legend_height / maximum)
144 xmin = 20
145 xmax = 100
146 xbuffer = 20
147 ybuffer = 30
148 draw.text(((xmax+xmin)/2, 200 - ybuffer), unit, text_color,
149 #font=title_font,
150 anchor='ms')
151 if title:
152 draw.text((im_width/2, text_size/2 + ybuffer), title, text_color,
153 #font=title_font,
154 anchor='ms')
155 mid_count = 0
156 for i in range(0, int(maximum)):
157 color = VisualizationUtils.interpolate_to_rgb(minimum, maximum, i)
158 color = tuple(tuple([int(color[0] * 255), int(color[1] * 255),
159 int(color[2] * 255)]))
160 draw.line([(xmin, 200 + i * line_width),
161 (xmax, 200 + i * line_width)], color, width=line_width)
162 if i == 0:
163 draw.text((xmax + xbuffer, 200 + i * line_width), str(i),
164 text_color, #font=title_font,
165 anchor='ms')
166 elif i % (int(maximum / 2)) == 0 and mid_count == 0:
167 mid_count += 1
168 blind_counter += 1
169 draw.text((xmax + xbuffer, 200 + i * line_width), str(i),
170 text_color, #font=title_font,
171 anchor='ms')
172 draw.text((xmax + xbuffer, 200 + int(maximum) * line_width),
173 str(int(maximum)), text_color, #font=title_font,
174 anchor='ms')
175 im.save(str(save_path).strip('.png') + '_legende.png')
177 @staticmethod
178 def visualize_zones(zone_dict, export_path, paths):
179 """Visualizes the thermalzones and saves the picture as a .png.
180 Fetches the thermalzones which are grouped before and creates an
181 abstract
182 building image, where each grouped zone has its own color. Afterwards a
183 legend is added with zone names and corresponding colors. The file is
184 exported as .png to the export folder.
185 Args:
186 zone_dict: dict that has a grouping string as key and list of
187 zones as
188 values.
189 folder_structure: instance of Folderstructure which is assigend
190 to the
191 projects attribute self.paths.
192 Returns:
193 No return value, image is saved directly.
194 """
196 # todo multi storage floor plan where all floors are placed
197 # next to each other
198 csv_name = export_path / 'eplusout.csv'
199 full_key_total_heat_rate = ' IDEAL LOADS AIR SYSTEM:Zone Ideal Loads ' \
200 'Zone Total Heating Rate [W](Hourly)'
201 zone_total_heating_rate = \
202 VisualizationUtils.get_column_from_ep_results(
203 csv_name=csv_name, column_key=full_key_total_heat_rate)
204 # get maximum total heating rate per zone
205 max_zone_total_heat_rate = zone_total_heating_rate.max()
206 # fill new dict with key: guid and value :zone net area
207 area_dict = {}
208 for i, (guid, zone) in enumerate(zone_dict.items()):
209 area_dict[guid.upper()] = zone.net_area.m
210 # add zone net heat area to maximum heat area dataframe
211 max_heat_rate_df = pd.DataFrame(max_zone_total_heat_rate,
212 columns=['max_zone_total_heat_rate'])
213 max_heat_rate_df['net_area'] = pd.DataFrame.from_dict(area_dict,
214 orient='index')
215 # compute maximum total heat rate per zone area (normalization)
216 max_heat_rate_df['max_per_area'] = \
217 max_heat_rate_df['max_zone_total_heat_rate'] / max_heat_rate_df[
218 'net_area']
220 # add colored 3D plot of space geometry
221 minimum = 0
222 maximum = max_heat_rate_df['max_per_area'].max()
224 settings = ifcopenshell.geom.settings()
225 settings.set(settings.USE_PYTHON_OPENCASCADE, True)
226 settings.set(settings.USE_WORLD_COORDS, True)
227 settings.set(settings.EXCLUDE_SOLIDS_AND_SURFACES, False)
228 settings.set(settings.INCLUDE_CURVES, True)
230 display, start_display, add_menu, add_function_to_menu = init_display(
231 display_triedron=False, background_gradient_color1=3 * [255],
232 background_gradient_color2=3 * [255], size=(1920, 1080))
234 for i, (guid, zone) in enumerate(zone_dict.items()):
235 current_value = max_heat_rate_df['max_per_area'].loc[guid.upper()]
236 color = VisualizationUtils.rgb_color(
237 VisualizationUtils.interpolate_to_rgb(minimum, maximum,
238 current_value))
239 display.DisplayShape(zone.space_shape, update=True, color=color,
240 transparency=0.5)
241 nr_zones = len(zone_dict)
242 filename = 'zonemodel_' + str(nr_zones) + '.png'
244 save_path = Path(export_path / filename)
245 display.View.Dump(str(save_path))
247 # add floorplan
248 floorplan_dict = {}
250 for i, (guid, zone) in enumerate(zone_dict.items()):
251 floorplan_dict[guid.upper()] = zone.footprint_shape
253 storey_list = list(set([zone.storey for i, (guid, zone) in
254 enumerate(zone_dict.items())]))
256 for storey in storey_list:
257 display, start_display, add_menu, add_function_to_menu = \
258 init_display(display_triedron=False,
259 background_gradient_color1=3 * [255],
260 background_gradient_color2=3 * [255],
261 size=(1920, 1080))
262 for zone in storey.thermal_zones:
263 guid = zone.guid
264 shape = floorplan_dict.get(guid.upper())
265 current_value = max_heat_rate_df['max_per_area'].loc[
266 guid.upper()]
267 color = VisualizationUtils.rgb_color(
268 VisualizationUtils.interpolate_to_rgb(minimum, maximum,
269 current_value))
270 display.DisplayShape(shape, update=True, color=color,
271 transparency=0)
272 center = PyOCCTools.get_center_of_face(shape)
273 if not center:
274 center = PyOCCTools.get_center_of_shape(shape)
275 display.DisplayMessage(
276 gp_Pnt(center.X(), center.Y(), center.Z() + 1.),
277 str(round(current_value, 2))+' W/m2',
278 message_color=(0, 0, 0))
279 display.View_Top()
280 display.FitAll()
281 this_floorplan_path = str(str(save_path).strip('.png') + storey.name +
282 '_floorplan.png')
283 display.View.Dump(this_floorplan_path)
284 VisualizationUtils.add_legend(
285 this_floorplan_path, paths, minimum, maximum,
286 unit='W/m²'
287 )
288 #todo: rescale underlying image so that the floorplan does not
289 # overlap with legend.
291 VisualizationUtils.add_legend(
292 save_path, paths, minimum, maximum, unit='W/m²',
293 title='Maximum total heating rate per area'
294 )