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

1"""holds functions to create a report for a TEASER project model""" 

2 

3import html 

4import os 

5import csv 

6from collections import OrderedDict 

7 

8import plotly.graph_objects as go 

9 

10 

11def localize_floats(row): 

12 return [str(el).replace(".", ",") if isinstance(el, float) else el for el in row] 

13 

14 

15def create_model_report(prj, path): 

16 """Creates model report for the project. 

17 

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. 

22 

23 Parameters 

24 ---------- 

25 

26 prj : Project 

27 project that the report should be created for 

28 path : string 

29 path of the base project export 

30 

31 """ 

32 

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 

50 

51 # todo use bldg.*_names if existing 

52 

53 prj_data[bldg_name]["Outerwall Area"] = {} 

54 outer_wall_area_total = 0 

55 

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"] = {} 

78 

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} 

83 

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] 

90 

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() 

96 

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 

137 

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 

142 

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 

147 

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 

152 

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 

159 

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 

172 

173 bldg_data = prj_data[bldg_name] 

174 

175 export_reports(bldg_data, bldg_name, path, prj) 

176 

177 

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) 

197 

198 

199def create_csv_report(bldg_data, output_path_base): 

200 # flat the keys 

201 

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] 

209 

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() 

218 

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 ] 

250 

251 keys = [""] 

252 keys.extend([x[0] for x in bldg_data_flat_sorted]) 

253 

254 values = ["TEASER"] 

255 values.extend([x[1] for x in bldg_data_flat_sorted]) 

256 

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)) 

262 

263 

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 ] 

271 

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 ) 

296 

297 arrow_length = 1 

298 arrow_color = "black" 

299 

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) 

314 

315 # Set layout 

316 fig.update_layout(scene=dict(aspectmode="manual", aspectratio=dict(x=1, y=1, z=1))) 

317 return fig 

318 

319 

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 """ 

384 

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" 

414 

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 

442 

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;">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span> 

498 Walls 

499 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 

500 <span class="badge badge-light" 

501 style="background-color: blue;">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</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""" 

532 

533 with open(html_file_name, "w") as html_file: 

534 html_file.write(html_content) 

535 

536 

537def create_simple_3d_visualization(bldg_data, roof_angle=30): 

538 """Creates a simplified 3d plot of the building. 

539 

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 """ 

554 

555 def get_value_with_default(lst, index, default_value): 

556 try: 

557 return lst[index] 

558 except IndexError: 

559 return default_value 

560 

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"] 

582 

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) 

587 

588 fig = go.Figure() 

589 

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 ) 

615 

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 ] 

655 

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 ] 

670 

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 ) 

687 

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 ) 

714 

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 ) 

773 

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