diff --git a/setup.py b/setup.py index 60fea021e..e65b58be9 100644 --- a/setup.py +++ b/setup.py @@ -81,5 +81,5 @@ "Topic :: Scientific/Engineering", "Topic :: Utilities", ], - install_requires=["mako", "pytest", "pandas", "numpy"], + install_requires=["mako", "pytest", "pandas", "numpy", "plotly"], ) diff --git a/teaser/data/output/reports/__init__.py b/teaser/data/output/reports/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/teaser/data/output/reports/model_report.py b/teaser/data/output/reports/model_report.py new file mode 100644 index 000000000..21def29f8 --- /dev/null +++ b/teaser/data/output/reports/model_report.py @@ -0,0 +1,764 @@ +"""holds functions to create a report for a TEASER project model""" + +import html +import os +import csv +from collections import OrderedDict + +import plotly.graph_objects as go + + +def localize_floats(row): + return [str(el).replace(".", ",") if isinstance(el, float) else el for el in row] + + +def create_model_report(prj, path): + """Creates model report for the project. + + This creates a html and .csv model report for each building of the project + for easier analysis of the created buildings. Currently only the basic + values for areas and U-values and an abstracted 3D visualization are part of + the report. Wall constructions and similar things might come in the future. + + Parameters + ---------- + + prj : Project + project that the report should be created for + path : string + path of the base project export + + """ + + prj_data = OrderedDict() + for bldg in prj.buildings: + bldg_name = bldg.name + prj_data[bldg_name] = OrderedDict() + # create keys + if bldg.type_of_building: + prj_data[bldg_name]["Type of Building"] = bldg.type_of_building + prj_data[bldg_name]["Net Ground Area"] = bldg.net_leased_area + prj_data[bldg_name]["Ground Floor Area"] = 0 + prj_data[bldg_name]["Roof Area"] = 0 + prj_data[bldg_name]["Floor Height"] = bldg.height_of_floors + prj_data[bldg_name]["Number of Floors"] = bldg.number_of_floors + prj_data[bldg_name]["Total Air Volume"] = bldg.volume + prj_data[bldg_name]["Number of Zones"] = len(bldg.thermal_zones) + prj_data[bldg_name]["Year of Construction"] = bldg.year_of_construction + prj_data[bldg_name]["Calculated Heat Load"] = bldg.sum_heat_load + prj_data[bldg_name]["Calculated Cooling Load"] = bldg.sum_cooling_load + + # todo use bldg.*_names if existing + + prj_data[bldg_name]["Outerwall Area"] = {} + outer_wall_area_total = 0 + + outer_areas = bldg.outer_area + # make sure that lowest values of orient come first + sorted_keys = sorted(outer_areas.keys()) + sorted_outer_areas = {key: outer_areas[key] for key in sorted_keys} + for orient in outer_areas: + # some archetypes use floats, some integers for orientation in + # TEASER + orient = float(orient) + if orient == -1: + prj_data[bldg_name]["Roof Area"] += sorted_outer_areas[orient] + elif orient == -2: + prj_data[bldg_name]["Ground Floor Area"] += sorted_outer_areas[orient] + else: + if orient not in prj_data[bldg_name]["Outerwall Area"]: + prj_data[bldg_name]["Outerwall Area"][orient] = 0 + prj_data[bldg_name]["Outerwall Area"][orient] += sorted_outer_areas[ + orient + ] + outer_wall_area_total += sorted_outer_areas[orient] + window_area_total = 0 + prj_data[bldg_name]["Outerwall Area Total"] = outer_wall_area_total + prj_data[bldg_name]["Window Area"] = {} + + window_areas = bldg.window_area + # make sure that lowest values of orient come first + sorted_keys = sorted(window_areas.keys()) + sorted_window_areas = {key: window_areas[key] for key in sorted_keys} + + for orient in sorted_window_areas: + orient = float(orient) + if orient not in prj_data[bldg_name]["Window Area"]: + prj_data[bldg_name]["Window Area"][orient] = 0 + prj_data[bldg_name]["Window Area"][orient] += sorted_window_areas[orient] + window_area_total += sorted_window_areas[orient] + + prj_data[bldg_name]["Window Area Total"] = window_area_total + prj_data[bldg_name]["Window-Wall-Ratio"] = ( + window_area_total / outer_wall_area_total + ) + prj_data[bldg_name]["Inner Wall Area"] = bldg.get_inner_wall_area() + + u_values_win = [] + g_values_windows = [] + u_values_ground_floor = [] + u_values_inner_wall = [] + u_values_outer_wall = [] + u_values_door = [] + u_values_roof = [] + u_values_ceiling = [] + for tz in bldg.thermal_zones: + for window in tz.windows: + u_values_win.append(1 / (window.r_conduc * window.area)) + g_values_windows.append(window.g_value) + for inner_wall in tz.inner_walls: + u_values_inner_wall.append(1 / (inner_wall.r_conduc * inner_wall.area)) + for outer_wall in tz.outer_walls: + u_values_outer_wall.append(1 / (outer_wall.r_conduc * outer_wall.area)) + for rooftop in tz.rooftops: + u_values_roof.append(1 / (rooftop.r_conduc * rooftop.area)) + for ground_floor in tz.ground_floors: + u_values_ground_floor.append( + 1 / (ground_floor.r_conduc * ground_floor.area) + ) + for ceiling in tz.ceilings: + u_values_ceiling.append(1 / (ceiling.r_conduc * ceiling.area)) + for floor in tz.floors: + u_values_ceiling.append(1 / (floor.r_conduc * floor.area)) + for door in tz.doors: + u_values_door.append(1 / (door.r_conduc * door.area)) + if len(u_values_outer_wall) > 0: + prj_data[bldg_name]["UValue Outerwall"] = sum(u_values_outer_wall) / len( + u_values_outer_wall + ) + else: + prj_data[bldg_name]["UValue Outerwall"] = 0 + if len(u_values_inner_wall) > 0: + prj_data[bldg_name]["UValue Innerwall"] = sum(u_values_inner_wall) / len( + u_values_inner_wall + ) + else: + prj_data[bldg_name]["UValue Innerwall"] = 0 + + if len(u_values_win) > 0: + prj_data[bldg_name]["UValue Window"] = sum(u_values_win) / len(u_values_win) + else: + prj_data[bldg_name]["UValue Window"] = 0 + + if len(u_values_door) > 0: + prj_data[bldg_name]["UValue Door"] = sum(u_values_door) / len(u_values_door) + else: + prj_data[bldg_name]["UValue Door"] = 0 + + if len(u_values_roof) > 0: + prj_data[bldg_name]["UValue Roof"] = sum(u_values_roof) / len(u_values_roof) + else: + prj_data[bldg_name]["UValue Roof"] = 0 + + if len(u_values_ceiling) > 0: + prj_data[bldg_name]["UValue Ceiling"] = sum(u_values_ceiling) / len( + u_values_ceiling + ) + else: + prj_data[bldg_name]["UValue Ceiling"] = 0 + + if len(u_values_ground_floor) > 0: + prj_data[bldg_name]["UValue Groundfloor"] = sum( + u_values_ground_floor + ) / len(u_values_ground_floor) + else: + prj_data[bldg_name]["UValue Groundfloor"] = 0 + if len(g_values_windows) > 0: + prj_data[bldg_name]["gValue Window"] = sum(g_values_windows) / len( + g_values_windows + ) + else: + prj_data[bldg_name]["gValue Window"] = 0 + + bldg_data = prj_data[bldg_name] + + export_reports(bldg_data, bldg_name, path, prj) + + +def export_reports(bldg_data, bldg_name, path, prj): + if not os.path.exists(path): + os.mkdir(path) + os.mkdir(os.path.join(path, "plots")) + base_name = f"{prj.name}_{bldg_name}" + output_path_base = os.path.join(path, base_name) + plotly_file_name = os.path.join(path, "plots", base_name + "_plotly.html") + # Draw an abstract image of the building and save it with plotly to HTML + interactive_fig = create_simple_3d_visualization(bldg_data, roof_angle=30) + if interactive_fig: + interactive_fig.write_html(plotly_file_name) + else: + plotly_file_name = None + html_file_name = os.path.join(output_path_base + ".html") + create_html_page(bldg_data, prj.name, bldg_name, html_file_name, plotly_file_name) + create_csv_report(bldg_data, output_path_base) + + +def create_csv_report(bldg_data, output_path_base): + # flat the keys + + prj_data_flat = {} + for key, val in bldg_data.items(): + if isinstance(bldg_data[key], dict): + for subkey in bldg_data[key].keys(): + prj_data_flat[str(key) + "_" + f"{subkey:03}"] = bldg_data[key][subkey] + else: + prj_data_flat[key] = bldg_data[key] + + bldg_add_list = {"OuterWall": [], "Window": []} + for key in prj_data_flat.keys(): + if key.startswith("Outerwall Area_"): + bldg_add_list["OuterWall"].append(key) + if key.startswith("Window Area_"): + bldg_add_list["Window"].append(key) + bldg_add_list["OuterWall"].sort() + bldg_add_list["Window"].sort() + + bldg_sorted_list = [ + "Net Ground Area", + "Number of Zones" "Ground Floor Area", + "Roof Area", + "Floor Height", + "Number of Floors", + "Total Air Volume", + *bldg_add_list["OuterWall"], + *bldg_add_list["Window"], + "Window-Wall-Ratio", + "Inner Wall Area", + "UValue Outerwall", + "UValue Innerwall", + "UValue Window", + "UValue Door", + "UValue Roof", + "UValue Ceiling", + "UValue Groundfloor", + "gValue Window", + ] + # round values + for key, value in prj_data_flat.items(): + if not isinstance(value, str): + prj_data_flat[key] = round(value, 2) + else: + prj_data_flat[key] = value + bldg_data_flat_sorted = [ + (k, prj_data_flat[k]) for k in bldg_sorted_list if k in prj_data_flat.keys() + ] + + keys = [""] + keys.extend([x[0] for x in bldg_data_flat_sorted]) + + values = ["TEASER"] + values.extend([x[1] for x in bldg_data_flat_sorted]) + + csv_file_name = os.path.join(output_path_base + ".csv") + with open(csv_file_name, "w", newline="", encoding="utf-8") as f: + csvwriter = csv.writer(f, delimiter=";") + csvwriter.writerow(keys) + csvwriter.writerow(localize_floats(values)) + + +def add_compass_to_3d_plot(fig, x_y_axis_sizing): + lines = [ + ((0, x_y_axis_sizing - 1, 0), (0, x_y_axis_sizing, 0), "N"), + ((x_y_axis_sizing - 1, 0, 0), (x_y_axis_sizing, 0, 0), "E"), + ((0, -x_y_axis_sizing + 1, 0), (0, -x_y_axis_sizing, 0), "S"), + ((-x_y_axis_sizing + 1, 0, 0), (-x_y_axis_sizing, 0, 0), "W"), + ] + + for start, end, label in lines: + fig.add_trace( + go.Scatter3d( + x=[start[0], end[0]], + y=[start[1], end[1]], + z=[start[2], end[2]], + mode="lines+text", + line=dict(color="black"), + hoverinfo="none", + showlegend=False, + ) + ) + fig.add_trace( + go.Scatter3d( + x=[end[0]], + y=[end[1]], + z=[end[2]], + mode="text", + text=[label], + textposition="top center", + hoverinfo="none", + showlegend=False, + ) + ) + + arrow_length = 1 + arrow_color = "black" + + arrow = go.Cone( + x=[end[0]], + y=[end[1]], + z=[end[2]], + u=[end[0] - start[0]], + v=[end[1] - start[1]], + w=[end[2] - start[2]], + sizemode="absolute", + sizeref=arrow_length, + showscale=False, + colorscale=[[0, arrow_color], [1, arrow_color]], + hoverinfo="none", + ) + fig.add_trace(arrow) + + # Set layout + fig.update_layout(scene=dict(aspectmode="manual", aspectratio=dict(x=1, y=1, z=1))) + return fig + + +def create_html_page(bldg_data, prj_name, bldg_name, html_file_name, iframe_src): + html_content = f""" + + +
+{html.escape(category)} | +||
---|---|---|
(0° := North, 90° := East, + 180° := South, 270° := West) | +||
{html.escape(str(key))} + {html.escape(str(orient))} | +{html.escape( + str(round(value, 2)))} | ++ {html.escape(unit)} | +
{html.escape(key_human_readable)} | + """ + if not isinstance(value, str): + value = str(round(value, 2)) + if not list_item: + html_content += f""" +{html.escape(value)} | +
+ """
+ if isinstance(unit, list):
+ html_content += f"""
+ {html.escape(unit[0])} |
+
Error: No graphic
+ available.
+ Error during image creation.