Coverage for teaser/data/output/reports/model_report.py: 93%
273 statements
« prev ^ index » next coverage.py v7.4.4, created at 2025-04-29 16:01 +0000
« prev ^ index » next coverage.py v7.4.4, created at 2025-04-29 16:01 +0000
1"""holds functions to create a report for a TEASER project model"""
3import html
4import os
5import csv
6from collections import OrderedDict
8import plotly.graph_objects as go
11def localize_floats(row):
12 return [str(el).replace(".", ",") if isinstance(el, float) else el for el in row]
15def create_model_report(prj, path):
16 """Creates model report for the project.
18 This creates a html and .csv model report for each building of the project
19 for easier analysis of the created buildings. Currently only the basic
20 values for areas and U-values and an abstracted 3D visualization are part of
21 the report. Wall constructions and similar things might come in the future.
23 Parameters
24 ----------
26 prj : Project
27 project that the report should be created for
28 path : string
29 path of the base project export
31 """
33 prj_data = OrderedDict()
34 for bldg in prj.buildings:
35 bldg_name = bldg.name
36 prj_data[bldg_name] = OrderedDict()
37 # create keys
38 if bldg.type_of_building:
39 prj_data[bldg_name]["Type of Building"] = bldg.type_of_building
40 prj_data[bldg_name]["Net Ground Area"] = bldg.net_leased_area
41 prj_data[bldg_name]["Ground Floor Area"] = 0
42 prj_data[bldg_name]["Roof Area"] = 0
43 prj_data[bldg_name]["Floor Height"] = bldg.height_of_floors
44 prj_data[bldg_name]["Number of Floors"] = bldg.number_of_floors
45 prj_data[bldg_name]["Total Air Volume"] = bldg.volume
46 prj_data[bldg_name]["Number of Zones"] = len(bldg.thermal_zones)
47 prj_data[bldg_name]["Year of Construction"] = bldg.year_of_construction
48 prj_data[bldg_name]["Calculated Heat Load"] = bldg.sum_heat_load
49 prj_data[bldg_name]["Calculated Cooling Load"] = bldg.sum_cooling_load
51 # todo use bldg.*_names if existing
53 prj_data[bldg_name]["Outerwall Area"] = {}
54 outer_wall_area_total = 0
56 outer_areas = bldg.outer_area
57 # make sure that lowest values of orient come first
58 sorted_keys = sorted(outer_areas.keys())
59 sorted_outer_areas = {key: outer_areas[key] for key in sorted_keys}
60 for orient in sorted_outer_areas:
61 # some archetypes use floats, some integers for orientation in
62 # TEASER
63 orient = float(orient)
64 if orient == -1:
65 prj_data[bldg_name]["Roof Area"] += sorted_outer_areas[orient]
66 elif orient == -2:
67 prj_data[bldg_name]["Ground Floor Area"] += sorted_outer_areas[orient]
68 else:
69 if orient not in prj_data[bldg_name]["Outerwall Area"]:
70 prj_data[bldg_name]["Outerwall Area"][orient] = 0
71 prj_data[bldg_name]["Outerwall Area"][orient] += sorted_outer_areas[
72 orient
73 ]
74 outer_wall_area_total += sorted_outer_areas[orient]
75 window_area_total = 0
76 prj_data[bldg_name]["Outerwall Area Total"] = outer_wall_area_total
77 prj_data[bldg_name]["Window Area"] = {}
79 window_areas = bldg.window_area
80 # make sure that lowest values of orient come first
81 sorted_keys = sorted(window_areas.keys())
82 sorted_window_areas = {key: window_areas[key] for key in sorted_keys}
84 for orient in sorted_window_areas:
85 orient = float(orient)
86 if orient not in prj_data[bldg_name]["Window Area"]:
87 prj_data[bldg_name]["Window Area"][orient] = 0
88 prj_data[bldg_name]["Window Area"][orient] += sorted_window_areas[orient]
89 window_area_total += sorted_window_areas[orient]
91 prj_data[bldg_name]["Window Area Total"] = window_area_total
92 prj_data[bldg_name]["Window-Wall-Ratio"] = (
93 window_area_total / outer_wall_area_total
94 )
95 prj_data[bldg_name]["Inner Wall Area"] = bldg.get_inner_wall_area()
97 u_values_win = []
98 g_values_windows = []
99 u_values_ground_floor = []
100 u_values_inner_wall = []
101 u_values_outer_wall = []
102 u_values_door = []
103 u_values_roof = []
104 u_values_ceiling = []
105 for tz in bldg.thermal_zones:
106 for window in tz.windows:
107 u_values_win.append(1 / (window.r_conduc * window.area))
108 g_values_windows.append(window.g_value)
109 for inner_wall in tz.inner_walls:
110 u_values_inner_wall.append(1 / (inner_wall.r_conduc * inner_wall.area))
111 for outer_wall in tz.outer_walls:
112 u_values_outer_wall.append(1 / (outer_wall.r_conduc * outer_wall.area))
113 for rooftop in tz.rooftops:
114 u_values_roof.append(1 / (rooftop.r_conduc * rooftop.area))
115 for ground_floor in tz.ground_floors:
116 u_values_ground_floor.append(
117 1 / (ground_floor.r_conduc * ground_floor.area)
118 )
119 for ceiling in tz.ceilings:
120 u_values_ceiling.append(1 / (ceiling.r_conduc * ceiling.area))
121 for floor in tz.floors:
122 u_values_ceiling.append(1 / (floor.r_conduc * floor.area))
123 for door in tz.doors:
124 u_values_door.append(1 / (door.r_conduc * door.area))
125 if len(u_values_outer_wall) > 0:
126 prj_data[bldg_name]["UValue Outerwall"] = sum(u_values_outer_wall) / len(
127 u_values_outer_wall
128 )
129 else:
130 prj_data[bldg_name]["UValue Outerwall"] = 0
131 if len(u_values_inner_wall) > 0:
132 prj_data[bldg_name]["UValue Innerwall"] = sum(u_values_inner_wall) / len(
133 u_values_inner_wall
134 )
135 else:
136 prj_data[bldg_name]["UValue Innerwall"] = 0
138 if len(u_values_win) > 0:
139 prj_data[bldg_name]["UValue Window"] = sum(u_values_win) / len(u_values_win)
140 else:
141 prj_data[bldg_name]["UValue Window"] = 0
143 if len(u_values_door) > 0:
144 prj_data[bldg_name]["UValue Door"] = sum(u_values_door) / len(u_values_door)
145 else:
146 prj_data[bldg_name]["UValue Door"] = 0
148 if len(u_values_roof) > 0:
149 prj_data[bldg_name]["UValue Roof"] = sum(u_values_roof) / len(u_values_roof)
150 else:
151 prj_data[bldg_name]["UValue Roof"] = 0
153 if len(u_values_ceiling) > 0:
154 prj_data[bldg_name]["UValue Ceiling"] = sum(u_values_ceiling) / len(
155 u_values_ceiling
156 )
157 else:
158 prj_data[bldg_name]["UValue Ceiling"] = 0
160 if len(u_values_ground_floor) > 0:
161 prj_data[bldg_name]["UValue Groundfloor"] = sum(
162 u_values_ground_floor
163 ) / len(u_values_ground_floor)
164 else:
165 prj_data[bldg_name]["UValue Groundfloor"] = 0
166 if len(g_values_windows) > 0:
167 prj_data[bldg_name]["gValue Window"] = sum(g_values_windows) / len(
168 g_values_windows
169 )
170 else:
171 prj_data[bldg_name]["gValue Window"] = 0
173 bldg_data = prj_data[bldg_name]
175 export_reports(bldg_data, bldg_name, path, prj)
178def export_reports(bldg_data, bldg_name, path, prj):
179 if not os.path.exists(path):
180 os.mkdir(path)
181 os.mkdir(os.path.join(path, "plots"))
182 base_name = f"{prj.name}_{bldg_name}"
183 output_path_base = os.path.join(path, base_name)
184 plotly_file_name = os.path.join(path, "plots", base_name + "_plotly.html")
185 # Draw an abstract image of the building and save it with plotly to HTML
186 interactive_fig, fixed_height =\
187 create_simple_3d_visualization(bldg_data, roof_angle=30)
188 if interactive_fig:
189 interactive_fig.write_html(plotly_file_name)
190 else:
191 plotly_file_name = None
192 html_file_name = os.path.join(output_path_base + ".html")
193 create_html_page(
194 bldg_data, prj.name, bldg_name, html_file_name, plotly_file_name,
195 fixed_height)
196 create_csv_report(bldg_data, output_path_base)
199def create_csv_report(bldg_data, output_path_base):
200 # flat the keys
202 prj_data_flat = {}
203 for key, val in bldg_data.items():
204 if isinstance(bldg_data[key], dict):
205 for subkey in bldg_data[key].keys():
206 prj_data_flat[str(key) + "_" + f"{subkey:03}"] = bldg_data[key][subkey]
207 else:
208 prj_data_flat[key] = bldg_data[key]
210 bldg_add_list = {"OuterWall": [], "Window": []}
211 for key in prj_data_flat.keys():
212 if key.startswith("Outerwall Area_"):
213 bldg_add_list["OuterWall"].append(key)
214 if key.startswith("Window Area_"):
215 bldg_add_list["Window"].append(key)
216 bldg_add_list["OuterWall"].sort()
217 bldg_add_list["Window"].sort()
219 bldg_sorted_list = [
220 "Net Ground Area",
221 "Number of Zones" "Ground Floor Area",
222 "Roof Area",
223 "Floor Height",
224 "Number of Floors",
225 "Total Air Volume",
226 *bldg_add_list["OuterWall"],
227 *bldg_add_list["Window"],
228 "Window-Wall-Ratio",
229 "Inner Wall Area",
230 "UValue Outerwall",
231 "UValue Innerwall",
232 "UValue Window",
233 "UValue Door",
234 "UValue Roof",
235 "UValue Ceiling",
236 "UValue Groundfloor",
237 "gValue Window",
238 ]
239 # round values
240 for key, value in prj_data_flat.items():
241 if not value:
242 value = "-"
243 elif not isinstance(value, str):
244 prj_data_flat[key] = round(value, 2)
245 else:
246 prj_data_flat[key] = value
247 bldg_data_flat_sorted = [
248 (k, prj_data_flat[k]) for k in bldg_sorted_list if k in prj_data_flat.keys()
249 ]
251 keys = [""]
252 keys.extend([x[0] for x in bldg_data_flat_sorted])
254 values = ["TEASER"]
255 values.extend([x[1] for x in bldg_data_flat_sorted])
257 csv_file_name = os.path.join(output_path_base + ".csv")
258 with open(csv_file_name, "w", newline="", encoding="utf-8") as f:
259 csvwriter = csv.writer(f, delimiter=";")
260 csvwriter.writerow(keys)
261 csvwriter.writerow(localize_floats(values))
264def add_compass_to_3d_plot(fig, x_y_axis_sizing):
265 lines = [
266 ((0, x_y_axis_sizing - 1, 0), (0, x_y_axis_sizing, 0), "<b>N</b>"),
267 ((x_y_axis_sizing - 1, 0, 0), (x_y_axis_sizing, 0, 0), "<b>E</b>"),
268 ((0, -x_y_axis_sizing + 1, 0), (0, -x_y_axis_sizing, 0), "<b>S</b>"),
269 ((-x_y_axis_sizing + 1, 0, 0), (-x_y_axis_sizing, 0, 0), "<b>W</b>"),
270 ]
272 for start, end, label in lines:
273 fig.add_trace(
274 go.Scatter3d(
275 x=[start[0], end[0]],
276 y=[start[1], end[1]],
277 z=[start[2], end[2]],
278 mode="lines+text",
279 line=dict(color="black"),
280 hoverinfo="none",
281 showlegend=False,
282 )
283 )
284 fig.add_trace(
285 go.Scatter3d(
286 x=[end[0]],
287 y=[end[1]],
288 z=[end[2]],
289 mode="text",
290 text=[label],
291 textposition="top center",
292 hoverinfo="none",
293 showlegend=False,
294 )
295 )
297 arrow_length = 1
298 arrow_color = "black"
300 arrow = go.Cone(
301 x=[end[0]],
302 y=[end[1]],
303 z=[end[2]],
304 u=[end[0] - start[0]],
305 v=[end[1] - start[1]],
306 w=[end[2] - start[2]],
307 sizemode="absolute",
308 sizeref=arrow_length,
309 showscale=False,
310 colorscale=[[0, arrow_color], [1, arrow_color]],
311 hoverinfo="none",
312 )
313 fig.add_trace(arrow)
315 # Set layout
316 fig.update_layout(scene=dict(aspectmode="manual", aspectratio=dict(x=1, y=1, z=1)))
317 return fig
320def create_html_page(bldg_data, prj_name, bldg_name, html_file_name,
321 iframe_src, fixed_height):
322 html_content = f"""
323 <!DOCTYPE html>
324 <html>
325 <head>
326 <title>{html.escape(prj_name)} - {html.escape(bldg_name)}</title>
327 <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
328 <style>
329 body {{
330 font-family: Arial, sans-serif;
331 background-color: #f8f9fa;
332 padding: 20px;
333 }}
334 .container {{
335 background-color: #ffffff;
336 border: 1px solid #e2e2e2;
337 border-radius: 5px;
338 padding: 20px;
339 box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.1);
340 }}
341 h1 {{
342 text-align: center;
343 margin-bottom: 20px;
344 }}
345 table {{
346 border-collapse: collapse;
347 }}
348 th, td {{
349 padding: 12px;
350 text-align: left;
351 border-bottom: 1px solid #dee2e6;
352 }}
353 th {{
354 background-color: #f8f9fa;
355 }}
356 .red-bg {{
357 background-color: #f44336;
358 color: #ffffff;
359 }}
360 .iframe-container {{
361 border: 1px solid #e2e2e2;
362 border-radius: 5px;
363 padding: 0px;
364 }}
365 iframe {{
366 width: 100%;
367 height: 600px;
368 border: none;
369 }}
370 .legend {{
371 margin-top: 10px;
372 font-size: 14px;
373 }}
374 </style>
375 </head>
376 <body>
377 <h1 class="red-bg py-2">{
378 html.escape(prj_name)} - {html.escape(bldg_name)}</h1>
379 <div class="container">
380 <div class="row">
381 <div class="col-md-6">
382 <table class="table table-bordered">
383 """
385 current_category = None
386 for key, value in bldg_data.items():
387 unit = "-"
388 category = None
389 list_item = False
390 # Handle category names
391 if (
392 "window" in key.lower() or "wall" in key.lower()
393 ) and "uvalue" not in key.lower():
394 category = "Wall and Window Areas"
395 unit = "m²"
396 elif key.startswith("UValue") or key.startswith("Gvalue"):
397 category = "U-Values (mean)"
398 unit = ["kW", "kg K"]
399 elif key in [
400 "Net Ground Area",
401 "Roof Area",
402 "Floor Height",
403 "Number of Floors",
404 "Total Air Volume",
405 "Number of Zones",
406 "Year of Construction",
407 "Type of Building",
408 ]:
409 category = "Base Values"
410 unit = "m²"
411 elif key.startswith("Calculated"):
412 category = "Calculated Values"
413 unit = "W"
415 if key.lower() in [
416 "number of floors",
417 "number of zones",
418 "year of construction",
419 "window-wall-ratio",
420 "gvalue window",
421 "type of building",
422 ]:
423 unit = "-"
424 if key.lower() == "total air volume":
425 unit = "m³"
426 if key.lower() == "floor height":
427 unit = "m"
428 if category and category != current_category:
429 html_content += f"""
430 <tr class="table-secondary">
431 <th colspan="3">{html.escape(category)}</th>
432 </tr>
433 """
434 if category == "Wall and Window Areas":
435 html_content += """
436 <tr>
437 <td colspan="2">(0° := North, 90° := East,
438 180° := South, 270° := West)</td>
439 </tr>
440 """
441 current_category = category
443 # handle subdict for outerwall and window area with directions
444 if key == "Outerwall Area" or key == "Window Area":
445 list_item = True
446 for orient, area in bldg_data[key].items():
447 value = area
448 html_content += f"""
449 <tr>
450 <th scope="row">{html.escape(str(key))}
451 {html.escape(str(orient))}</th>
452 <td>{html.escape(
453 str(round(value, 2)))} </td>
454 <td style=
455 "text-align: center; background-color: #D3D3D3;">
456 {html.escape(unit)}</td>
457 </tr>
458 """
459 else:
460 key_human_readable = " ".join(
461 [word.capitalize() for word in key.split("_")]
462 )
463 html_content += f"""
464 <tr>
465 <th scope="row">{html.escape(key_human_readable)}</th>
466 """
467 if not isinstance(value, str):
468 if value:
469 value = str(round(value, 2))
470 else:
471 value = "-"
472 if not list_item:
473 html_content += f"""
474 <td>{html.escape(value)} </td>
475 <td style=
476 "text-align: center; background-color: #D3D3D3;">
477 """
478 if isinstance(unit, list):
479 html_content += f"""
480 {html.escape(unit[0])} <frac> {html.escape(unit[1])}</td>
481 </tr>
482 """
483 else:
484 html_content += f"""
485 {html.escape(unit)}</td>
486 </tr>
487 """
488 if iframe_src:
489 html_content += f"""
490 </table>
491 </div>
492 <div class="col-md-6">
493 <div class="iframe-container">
494 <iframe src="{iframe_src}"></iframe>
495 <div class="legend">
496 <span class="badge badge-light"
497 style="background-color: gray;"> </span>
498 Walls
499
500 <span class="badge badge-light"
501 style="background-color: blue;"> </span>
502 Windows <br>"""
503 else:
504 html_content += f"""
505 </table>
506 </div>
507 <div class="col-md-6">
508 <div class="iframe-container">
509 <p style="color:Red"><b>Error: No graphic
510 available.
511 Error during image creation.</b> <br></p>"""
512 html_content += f"""
513 <i>Assumptions</i>: <br>
514 <li><i>All windows of a storey and with the same
515 orientation are put together into one big window
516 which is placed in the middle of the storey</i></li>
517 <li><i>Only works for buildings with 4 directions currently,
518 while the smallest will be interpreted as
519 north, the next bigger one as east and so on.</i></li>
520 <li><i>The roof is not displayed correctly yet</i></li>"""
521 if fixed_height:
522 html_content += f"""<li><i>The height of all floors is assumed to be 3
523 meters.</i></li>"""
524 html_content += f"""
525 </div>
526 </div>
527 </div>
528 </div>
529 </body>
530</html>
531"""
533 with open(html_file_name, "w") as html_file:
534 html_file.write(html_content)
537def create_simple_3d_visualization(bldg_data, roof_angle=30):
538 """Creates a simplified 3d plot of the building.
540 This is for a rough first visual analysis of the building and is mostly
541 relevant for buildings that are created "manual" and not for archetypes.
542 The simplified visualization has multiple assumptions/simplifications:
543 * All windows of a storey and with the same orientation are put together
544 into one big window which is placed in the middle of the storey
545 * Only works for buildings with 4 directions currently, while the smallest
546 will be interpreted as north, the next bigger one as east and so on.
547 * Orientations are
548 Positive y: North
549 Positive x: East
550 Negative y: South
551 Negative x: West
552 * The roof is not displayed correctly yet # TODO
553 """
555 def get_value_with_default(lst, index, default_value):
556 try:
557 return lst[index]
558 except IndexError:
559 return default_value
561 try:
562 area_values = list(bldg_data["Outerwall Area"].values())
563 window_values = list(bldg_data["Window Area"].values())
564 # TODO: use orientations as well and "turn" the vertices based on this.
565 # Currently the first value (which is the smallest) will be taken as
566 # north, the next one as east and so on. Only the first 4 values are
567 # taken into account.
568 area_north = get_value_with_default(area_values, 0, 0)
569 area_east = get_value_with_default(area_values, 1, 0)
570 area_south = get_value_with_default(area_values, 2, 0)
571 area_west = get_value_with_default(area_values, 3, 0)
572 window_area_north = get_value_with_default(window_values, 0, 0)
573 window_area_east = get_value_with_default(window_values, 1, 0)
574 window_area_south = get_value_with_default(window_values, 2, 0)
575 window_area_west = get_value_with_default(window_values, 3, 0)
576 height = bldg_data["Floor Height"]
577 fixed_height = False
578 if not height:
579 height = 3
580 fixed_height = True
581 num_floors = bldg_data["Number of Floors"]
583 length_north = area_north / (num_floors * height)
584 length_east = area_east / (num_floors * height)
585 length_south = area_south / (num_floors * height)
586 length_west = area_west / (num_floors * height)
588 fig = go.Figure()
590 fig.update_layout(
591 paper_bgcolor="rgba(0,0,0,0)",
592 plot_bgcolor="rgba(0,0,0,0)",
593 margin=dict(l=5, r=5, b=5, t=0),
594 scene=dict(
595 xaxis=dict(
596 gridcolor="white",
597 showbackground=False,
598 zerolinecolor="white",
599 ),
600 yaxis=dict(
601 gridcolor="white", showbackground=False, zerolinecolor="white"
602 ),
603 zaxis=dict(
604 gridcolor="white", showbackground=False, zerolinecolor="white"
605 ),
606 aspectmode="cube",
607 xaxis_showgrid=False,
608 yaxis_showgrid=False,
609 zaxis_showgrid=False,
610 xaxis_title="",
611 yaxis_title="",
612 zaxis_title="",
613 ),
614 )
616 max_length = max(length_north, length_south, length_west, length_east)
617 x_y_axis_sizing = (max_length / 2) * 1.1
618 fig.update_layout(
619 scene=dict(
620 xaxis=dict(range=[-x_y_axis_sizing, x_y_axis_sizing]),
621 yaxis=dict(range=[-x_y_axis_sizing, x_y_axis_sizing]),
622 zaxis=dict(range=[0, max_length]),
623 )
624 )
625 fig = add_compass_to_3d_plot(fig, x_y_axis_sizing)
626 for floor in range(num_floors):
627 # Ecken des aktuellen Stockwerks
628 floor_height = height * floor
629 vertices = [
630 (-length_south / 2, -length_east / 2, floor_height),
631 (-length_south / 2 + length_north, -length_east / 2, floor_height),
632 (
633 -length_south / 2 + length_north,
634 -length_east / 2 + length_west,
635 floor_height,
636 ),
637 (-length_south / 2, -length_east / 2 + length_west, floor_height),
638 (-length_south / 2, -length_east / 2, floor_height + height),
639 (
640 -length_south / 2 + length_north,
641 -length_east / 2,
642 floor_height + height,
643 ),
644 (
645 -length_south / 2 + length_north,
646 -length_east / 2 + length_west,
647 floor_height + height,
648 ),
649 (
650 -length_south / 2,
651 -length_east / 2 + length_west,
652 floor_height + height,
653 ),
654 ]
656 edges = [
657 # 0: bottom
658 [vertices[0], vertices[1], vertices[2], vertices[3], vertices[0]],
659 # 1: top
660 [vertices[4], vertices[5], vertices[6], vertices[7], vertices[4]],
661 # 2: south
662 [vertices[0], vertices[1], vertices[5], vertices[4], vertices[0]],
663 # 3: north
664 [vertices[2], vertices[3], vertices[7], vertices[6], vertices[2]],
665 # 4: east
666 [vertices[1], vertices[2], vertices[6], vertices[5], vertices[1]],
667 # 5: west
668 [vertices[4], vertices[7], vertices[3], vertices[0], vertices[4]],
669 ]
671 # Add walls as 3D polygons with color fill
672 for edge in edges:
673 xs, ys, zs = zip(*edge)
674 fig.add_trace(
675 go.Mesh3d(
676 x=xs,
677 y=ys,
678 z=zs,
679 i=[0, 0, 1, 0],
680 j=[1, 2, 2, 3],
681 k=[2, 3, 3, 1],
682 opacity=0.25,
683 color="gray",
684 hoverinfo="none",
685 )
686 )
688 # Fenster hinzufügen
689 window_gap_top_bottom = 0.5
690 for i, (window_area, wall_vertices) in enumerate(
691 zip(
692 [
693 window_area_north,
694 window_area_east,
695 window_area_south,
696 window_area_west,
697 ],
698 [edges[3], edges[4], edges[2], edges[5]],
699 )
700 ):
701 window_height = height - window_gap_top_bottom
702 window_width = window_area / (num_floors * window_height)
703 window_x_center = (
704 wall_vertices[0][0]
705 + (wall_vertices[1][0] - wall_vertices[0][0]) / 2
706 )
707 window_y_center = (
708 wall_vertices[0][1]
709 + (wall_vertices[2][1] - wall_vertices[0][1]) / 2
710 )
711 window_z_center = (
712 floor_height + window_gap_top_bottom / 2 + window_height / 2
713 )
715 if i == 0 or i == 2:
716 fig.add_trace(
717 go.Mesh3d(
718 x=[
719 window_x_center - window_width / 2,
720 window_x_center + window_width / 2,
721 window_x_center + window_width / 2,
722 window_x_center - window_width / 2,
723 ],
724 y=[
725 window_y_center,
726 window_y_center,
727 window_y_center,
728 window_y_center,
729 ],
730 z=[
731 window_z_center - window_height / 2,
732 window_z_center - window_height / 2,
733 window_z_center + window_height / 2,
734 window_z_center + window_height / 2,
735 ],
736 i=[0, 0, 1, 0],
737 j=[1, 2, 2, 3],
738 k=[2, 3, 3, 1],
739 opacity=0.7,
740 color="blue",
741 hoverinfo="none",
742 )
743 )
744 else:
745 fig.add_trace(
746 go.Mesh3d(
747 x=[
748 window_x_center,
749 window_x_center,
750 window_x_center,
751 window_x_center,
752 ],
753 y=[
754 window_y_center - window_width / 2,
755 window_y_center + window_width / 2,
756 window_y_center + window_width / 2,
757 window_y_center - window_width / 2,
758 ],
759 z=[
760 window_z_center - window_height / 2,
761 window_z_center - window_height / 2,
762 window_z_center + window_height / 2,
763 window_z_center + window_height / 2,
764 ],
765 i=[0, 0, 1, 0],
766 j=[1, 2, 2, 3],
767 k=[2, 3, 3, 1],
768 opacity=0.7,
769 color="blue",
770 hoverinfo="none",
771 )
772 )
774 return fig, fixed_height
775 except Exception as e:
776 message = type(e).__name__ + str(e.args)
777 print(
778 f"An error occured during creating the simplified plot for model "
779 f"report. Will continue without plot. Error: {message}: "
780 )
781 return None