diff --git a/energytool/tools.py b/energytool/tools.py index 61b5a44..f77bf37 100644 --- a/energytool/tools.py +++ b/energytool/tools.py @@ -1,5 +1,8 @@ import pandas as pd import datetime as dt +import numpy as np +import plotly.graph_objects as go +import plotly.colors as pc def to_list(f_input): @@ -99,4 +102,412 @@ def add_day_in_period(self, start, end, days, hourly_dict): self.series.loc[selected_timestamp] = hourly_lst_from_dict(hourly_dict) * int( len(selected_timestamp) / 24 - ) \ No newline at end of file + ) + + +ZONE_PALETTE = ( + pc.qualitative.Safe + + pc.qualitative.Set3 +) +ADIABATIC_COLOR = "coral" +UNCONDITIONED_COLOR = "#9ECAE1" +WINDOW_COLOR = "cyan" +SHADING_COLOR = "mediumpurple" +CONDITIONED_ZONE_PALETTE = [ + "#67000d", + "#a50f15", + "#cb181d", + "#ef3b2c", + "#fb6a4a", + "#fc9272", + "#fcbba1", +] + +def plot_idf_geometry( + building, + show_building_surfaces=True, + show_fenestration_surfaces=True, + show_shading_surfaces=True, + show_names=False, + opacity=0.7, + color_mode="surface_type", +): + """ + Interactive 3D visualization of an EnergyPlus building geometry. + + This function displays the geometry contained in an IDF model using + Plotly. Building surfaces, fenestration surfaces and shading surfaces + can be visualized independently. The resulting figure can be explored + interactively (rotation, zoom, pan). + + Parameters + ---------- + building : Building + EnergyTool Building object containing an IDF model. + + show_building_surfaces : bool, default=True + Display BuildingSurface:Detailed objects. + + show_fenestration_surfaces : bool, default=True + Display FenestrationSurface:Detailed objects. + + show_shading_surfaces : bool, default=True + Display Shading:Zone:Detailed objects. + + show_names : bool, default=False + Display labels on the geometry. + + - In ``surface_type`` mode, surface names are displayed. + - In ``zone`` mode, thermal zone names are displayed at the + centroid of each zone. + + opacity : float, default=0.7 + Surface opacity between 0 and 1. + + color_mode : {"surface_type", "zone"}, default="surface_type" + Controls how surfaces are colored. + + ``surface_type``: + - External walls: light grey + - Internal walls: khaki + - Roofs: dark grey + - Floors: grey + - Windows: cyan + - Shading surfaces: purple + + ``zone``: + Colors are assigned according to thermal zone type. + + - Conditioned zones: red color palette + - Adiabatic zones: orange/coral + - Unconditioned zones: blue + + Returns + ------- + plotly.graph_objects.Figure + Interactive Plotly figure. + + Notes + ----- + This function is intended for model inspection and debugging. + + Typical use cases include: + + - Checking generated geometry + - Verifying window locations + - Validating shading modifiers + (overhangs, side fins, vegetation, PV systems, etc.) + - Visualizing thermal zoning + - Inspecting adiabatic and conditioned zones + + Examples + -------- + Display the complete building geometry: + + >>> plot_idf_geometry(building).show() + + Display thermal zones: + + >>> plot_idf_geometry( + ... building, + ... color_mode="zone", + ... show_names=True, + ... ).show() + + Display only windows and shading devices: + + >>> plot_idf_geometry( + ... building, + ... show_building_surfaces=False, + ... ).show() + + Visualize the effect of a shading modifier: + + >>> set_shading_geometry( + ... building, + ... shading_type="overhang", + ... description={"Depth": 1.0}, + ... ) + >>> + >>> plot_idf_geometry(building).show() + """ + + fig = go.Figure() + + groups = {} + _label_traces = [] + + def get_vertices(surface): + n_vertices = int(surface.Number_of_Vertices) + + return np.array( + [ + [ + float(getattr(surface, f"Vertex_{i}_Xcoordinate")), + float(getattr(surface, f"Vertex_{i}_Ycoordinate")), + float(getattr(surface, f"Vertex_{i}_Zcoordinate")), + ] + for i in range(1, n_vertices + 1) + ] + ) + + def _ensure_group(key, display_name, color): + if key not in groups: + groups[key] = dict( + name=display_name, + color=color, + x=[], y=[], z=[], + i=[], j=[], k=[], + text=[], + outlines=[], + ) + return groups[key] + + def add_surface(vertices, group_key, display_name, color, name=None): + if len(vertices) < 3: + return + + group = _ensure_group(group_key, display_name, color) + offset = len(group["x"]) + group["x"].extend(vertices[:, 0].tolist()) + group["y"].extend(vertices[:, 1].tolist()) + group["z"].extend(vertices[:, 2].tolist()) + group["text"].extend([name or ""] * len(vertices)) + + for idx in range(1, len(vertices) - 1): + group["i"].append(offset) + group["j"].append(offset + idx) + group["k"].append(offset + idx + 1) + + vertices_closed = np.vstack([vertices, vertices[0]]) + group["outlines"].append( + go.Scatter3d( + x=vertices_closed[:, 0], + y=vertices_closed[:, 1], + z=vertices_closed[:, 2], + mode="lines", + line=dict(color="black", width=2), + showlegend=False, + legendgroup=group_key, + hoverinfo="skip", + ) + ) + + if show_names and color_mode != "zone": + centroid = vertices.mean(axis=0) + _label_traces.append( + go.Scatter3d( + x=[centroid[0]], + y=[centroid[1]], + z=[centroid[2]], + mode="text", + text=[name], + showlegend=False, + ) + ) + + def add_zone_labels(building): + + zone_vertices = {} + + for surface in building.idf.idfobjects[ + "BUILDINGSURFACE:DETAILED" + ]: + + zone = getattr(surface, "Zone_Name", None) + + if not zone: + continue + + vertices = get_vertices(surface) + + zone_vertices.setdefault(zone, []).append( + vertices + ) + + for zone, surfaces in zone_vertices.items(): + all_vertices = np.vstack(surfaces) + + centroid = all_vertices.mean(axis=0) + centroid[2] += 0.5 + fig.add_trace( + go.Scatter3d( + x=[centroid[0]], + y=[centroid[1]], + z=[centroid[2]], + mode="text", + text=[zone], + showlegend=False, + ) + ) + + def get_zone_types(building): + + zone_types = {} + + for zone in building.idf.idfobjects["ZONE"]: + zone_types[zone.Name] = "unconditioned" + + for thermostat in building.idf.idfobjects.get( + "ZONECONTROL:THERMOSTAT", + [] + ): + zone_types[ + thermostat.Zone_or_ZoneList_Name + ] = "conditioned" + + for surface in building.idf.idfobjects[ + "BUILDINGSURFACE:DETAILED" + ]: + + zone = getattr(surface, "Zone_Name", None) + + if not zone: + continue + + boundary = getattr( + surface, + "Outside_Boundary_Condition", + "", + ).upper() + + if boundary == "ADIABATIC": + zone_types[zone] = "adiabatic" + + return zone_types + + def get_zone_colors(building): + + zone_types = get_zone_types(building) + + zones = sorted( + { + surface.Zone_Name + for surface in building.idf.idfobjects[ + "BUILDINGSURFACE:DETAILED" + ] + if getattr(surface, "Zone_Name", None) + } + ) + + colors = {} + + idx = 0 + + for zone in zones: + + if zone_types.get(zone) == "adiabatic": + colors[zone] = ADIABATIC_COLOR + + else: + zone_type = zone_types.get( + zone, + "conditioned" + ) + + if zone_type == "adiabatic": + + colors[zone] = ADIABATIC_COLOR + + elif zone_type == "unconditioned": + + colors[zone] = UNCONDITIONED_COLOR + + else: + + colors[zone] = ( + CONDITIONED_ZONE_PALETTE[ + idx % len( + CONDITIONED_ZONE_PALETTE + ) + ] + ) + + idx += 1 + + return colors + + zone_types = get_zone_types(building) + zone_colors = get_zone_colors(building) + + def get_building_surface_group(surface): + if color_mode == "zone": + zone_name = getattr(surface, "Zone_Name", None) or "unknown" + z_type = zone_types.get(zone_name, "unconditioned") + label = zone_name + (" (adiabatic)" if z_type == "adiabatic" else "") + return zone_name, label, zone_colors.get(zone_name, "lightgray") + stype = surface.Surface_Type.upper() + boundary = getattr(surface, "Outside_Boundary_Condition", "").upper() + if stype == "WALL": + if boundary == "OUTDOORS": + return "ext_wall", "External walls", "lightgray" + return "int_wall", "Internal walls", "khaki" + if stype == "ROOF": + return "roof", "Roofs", "dimgray" + if stype == "FLOOR": + return "floor", "Floors", "gray" + if stype == "CEILING": + return "ceiling", "Ceilings", "silver" + return "other", "Other", "lightgray" + + if show_building_surfaces: + for surface in building.idf.idfobjects["BUILDINGSURFACE:DETAILED"]: + key, label, color = get_building_surface_group(surface) + add_surface(get_vertices(surface), key, label, color, surface.Name) + + if show_fenestration_surfaces: + for surface in building.idf.idfobjects["FENESTRATIONSURFACE:DETAILED"]: + add_surface( + get_vertices(surface), "fenestration", "Windows", WINDOW_COLOR, surface.Name, + ) + + if show_shading_surfaces: + for surface in building.idf.idfobjects["SHADING:ZONE:DETAILED"]: + add_surface( + get_vertices(surface), "shading", "Shading", SHADING_COLOR, surface.Name, + ) + + for key, group in groups.items(): + fig.add_trace( + go.Mesh3d( + x=group["x"], + y=group["y"], + z=group["z"], + i=group["i"], + j=group["j"], + k=group["k"], + color=group["color"], + opacity=opacity, + text=group["text"], + hoverinfo="text", + showscale=False, + name=group["name"], + legendgroup=key, + showlegend=True, + ) + ) + for outline in group["outlines"]: + fig.add_trace(outline) + + for trace in _label_traces: + fig.add_trace(trace) + + if show_names and color_mode == "zone": + add_zone_labels(building) + + fig.update_layout( + scene=dict( + aspectmode="data", + xaxis_title="X", + yaxis_title="Y", + zaxis_title="Z", + ), + height=900, + margin=dict(l=0, r=0, b=0, t=20), + ) + + fig.update_layout( + legend=dict(yanchor="top", y=1, xanchor="left", x=1.02) + ) + + return fig diff --git a/tests/test_tools.py b/tests/test_tools.py index 623a985..43657b5 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -1,5 +1,15 @@ +from io import StringIO +from pathlib import Path + +import eppy +import plotly.graph_objects as go +import pytest +from eppy.modeleditor import IDF + import energytool.tools as tl +RESOURCES_PATH = Path(__file__).parent / "resources" + class TestTools: def test_select_by_strings(self): @@ -150,3 +160,149 @@ def test_scheduler(self): ] assert ref == list(test_scheduler.series.loc["2009-01-02":"2009-01-03"]) + + +class FakeBuilding: + def __init__(self, idf): + self.idf = idf + + +@pytest.fixture(scope="module") +def geo_building(): + try: + IDF.setiddname(RESOURCES_PATH / "Energy+.idd") + except eppy.modeleditor.IDDAlreadySetError: + pass + + idf = IDF(StringIO("")) + idf.idfname = None + + idf.newidfobject(key="Zone", Name="ConditionedZone") + idf.newidfobject(key="Zone", Name="UnconditionedZone") + idf.newidfobject( + key="ZoneControl:Thermostat", + Zone_or_ZoneList_Name="ConditionedZone", + ) + + def add_surface(obj_type, name, vertices, **attrs): + s = idf.newidfobject(key=obj_type, Name=name) + for k, v in attrs.items(): + setattr(s, k, v) + s.Number_of_Vertices = len(vertices) + for idx, (x, y, z) in enumerate(vertices, start=1): + setattr(s, f"Vertex_{idx}_Xcoordinate", x) + setattr(s, f"Vertex_{idx}_Ycoordinate", y) + setattr(s, f"Vertex_{idx}_Zcoordinate", z) + return s + + # Simple 3 m × 3 m × 3 m box — each surface at a distinct position + add_surface( + "BuildingSurface:Detailed", "ExtWall", + [(0, 0, 0), (3, 0, 0), (3, 0, 3), (0, 0, 3)], + Surface_Type="Wall", + Outside_Boundary_Condition="Outdoors", + Zone_Name="ConditionedZone", + ) + add_surface( + "BuildingSurface:Detailed", "IntWall", + [(0, 3, 0), (0, 3, 3), (3, 3, 3), (3, 3, 0)], + Surface_Type="Wall", + Outside_Boundary_Condition="Surface", + Zone_Name="ConditionedZone", + ) + add_surface( + "BuildingSurface:Detailed", "Roof", + [(0, 0, 3), (3, 0, 3), (3, 3, 3), (0, 3, 3)], + Surface_Type="Roof", + Outside_Boundary_Condition="Outdoors", + Zone_Name="ConditionedZone", + ) + add_surface( + "BuildingSurface:Detailed", "Floor", + [(0, 0, 0), (0, 3, 0), (3, 3, 0), (3, 0, 0)], + Surface_Type="Floor", + Outside_Boundary_Condition="Ground", + Zone_Name="ConditionedZone", + ) + add_surface( + "BuildingSurface:Detailed", "AdiabWall", + [(0, 0, 0), (0, 0, 3), (0, 3, 3), (0, 3, 0)], + Surface_Type="Wall", + Outside_Boundary_Condition="Adiabatic", + Zone_Name="UnconditionedZone", + ) + add_surface( + "FenestrationSurface:Detailed", "Window1", + [(0.5, 0, 0.5), (1.5, 0, 0.5), (1.5, 0, 2.0), (0.5, 0, 2.0)], + ) + add_surface( + "Shading:Zone:Detailed", "Overhang1", + [(-0.5, 0, 2.5), (2.5, 0, 2.5), (2.5, -1, 2.5), (-0.5, -1, 2.5)], + ) + + return FakeBuilding(idf) + + +class TestPlotIdfGeometry: + def test_returns_figure(self, geo_building): + fig = tl.plot_idf_geometry(geo_building) + assert isinstance(fig, go.Figure) + assert len(fig.data) == 13 + + def test_hide_surfaces(self, geo_building): + # Only fenestration + shading groups → 2 Mesh3d + 2 outlines + fig = tl.plot_idf_geometry(geo_building, show_building_surfaces=False) + assert len(fig.data) == 4 + fig = tl.plot_idf_geometry(geo_building, show_fenestration_surfaces=False) + assert len(fig.data) == 11 + fig = tl.plot_idf_geometry(geo_building, show_shading_surfaces=False) + assert len(fig.data) == 11 + + def test_surface_type_colors(self, geo_building): + fig = tl.plot_idf_geometry( + geo_building, + show_fenestration_surfaces=False, + show_shading_surfaces=False, + ) + color_by_name = {t.name: t.color for t in fig.data if isinstance(t, go.Mesh3d)} + assert color_by_name["External walls"] == "lightgray" + assert color_by_name["Internal walls"] == "khaki" + assert color_by_name["Roofs"] == "dimgray" + assert color_by_name["Floors"] == "gray" + + def test_zone_color_mode(self, geo_building): + # 2 zone groups + fenestration + shading → 4 Mesh3d + 7 outlines + fig = tl.plot_idf_geometry(geo_building, color_mode="zone") + assert len(fig.data) == 11 + mesh_names = {t.name for t in fig.data if isinstance(t, go.Mesh3d)} + assert "ConditionedZone" in mesh_names + assert "UnconditionedZone (adiabatic)" in mesh_names + + def test_legend_is_interactive(self, geo_building): + fig = tl.plot_idf_geometry(geo_building) + mesh_traces = [t for t in fig.data if isinstance(t, go.Mesh3d)] + assert all(t.showlegend for t in mesh_traces) + outline_traces = [ + t for t in fig.data + if isinstance(t, go.Scatter3d) and t.mode == "lines" + ] + assert all(t.legendgroup is not None for t in outline_traces) + + def test_show_names_surface_type_mode(self, geo_building): + fig_without = tl.plot_idf_geometry(geo_building, show_names=False) + fig_with = tl.plot_idf_geometry(geo_building, show_names=True) + fig_with.show() + text_without = [t for t in fig_without.data if isinstance(t, go.Scatter3d) and t.mode == "text"] + text_with = [t for t in fig_with.data if isinstance(t, go.Scatter3d) and t.mode == "text"] + assert len(text_without) == 0 + assert len(text_with) == 7 # one label per surface (5 building + 1 fenestration + 1 shading) + + def test_show_names_zone_mode(self, geo_building): + fig = tl.plot_idf_geometry(geo_building, color_mode="zone", show_names=True) + text_traces = [t for t in fig.data if isinstance(t, go.Scatter3d) and t.mode == "text"] + assert len(text_traces) == 2 # one centroid label per zone + + def test_opacity(self, geo_building): + fig = tl.plot_idf_geometry(geo_building, opacity=0.3) + mesh_traces = [t for t in fig.data if isinstance(t, go.Mesh3d)] + assert all(t.opacity == 0.3 for t in mesh_traces) diff --git a/tutorials/CH3_Building_Modifier.ipynb b/tutorials/CH3_Building_Modifier.ipynb index f4d3c5f..f070cfd 100644 --- a/tutorials/CH3_Building_Modifier.ipynb +++ b/tutorials/CH3_Building_Modifier.ipynb @@ -734,6 +734,355 @@ "outputs": [], "execution_count": null }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "# 1.3 Shading modifiers\n", + "\n", + "EnergyTool provides several modifiers to create and manipulate solar shading systems.\n", + "\n", + "These modifiers can be grouped into three categories:\n", + "\n", + "### Geometric shadings\n", + "\n", + "Physical geometries added around windows:\n", + "\n", + "- `overhang`\n", + "- `sidefins`\n", + "- `horizontal_louvers`\n", + "- `vertical_louvers`\n", + "\n", + "These objects create actual `Shading:Zone:Detailed` surfaces in EnergyPlus and can be visualized directly using `plot_idf_geometry()`.\n", + "\n", + "Functions:\n", + "\n", + "```python\n", + "set_shading_geometry(...)\n", + "set_shading_properties(...)\n", + "set_shading_object(...)\n", + "```\n", + "\n", + "### Optical properties\n", + "\n", + "Optical properties can be assigned to shading surfaces:\n", + "\n", + "- solar reflectance\n", + "- visible reflectance\n", + "- glazing fraction\n", + "- transmittance schedules\n", + "\n", + "Function:\n", + "\n", + "```python\n", + "set_shading_properties(...)\n", + "```\n", + "\n", + "### Shades and blinds\n", + "\n", + "EnergyPlus also supports shading systems attached directly to glazing:\n", + "\n", + "- `Shade` (textile screens, roller shades)\n", + "- `Blind` (venetian blinds, BSO)\n", + "\n", + "Functions:\n", + "\n", + "```python\n", + "set_shade(...)\n", + "set_blind(...)\n", + "```\n" + ] + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "One of the simplest geometric shading systems is an overhang.\n", + "\n", + "The `set_shading_geometry()` modifier automatically generates horizontal shading surfaces above each selected window.\n", + "\n", + "Because these objects are created as explicit `Shading:Zone:Detailed` surfaces, they are directly considered by EnergyPlus during solar calculations and can be visualized with `plot_idf_geometry()`.\n", + "\n", + "\n", + "Let's plot the geometry without modifications first:" + ] + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "from energytool.tools import plot_idf_geometry\n", + "\n", + "plot_idf_geometry(\n", + " building,\n", + " color_mode=\"surface_type\",\n", + ")" + ], + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "Then, with an overhang added to all windows:" + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "from energytool.modifier import set_shading_geometry\n", + "from copy import deepcopy\n", + "\n", + "overhang_building = deepcopy(building)\n", + "\n", + "set_shading_geometry(\n", + " model=overhang_building,\n", + " shading_type=\"overhang\",\n", + " description={\n", + " \"Depth\": 0.4,\n", + " },\n", + ")\n", + "\n", + "plot_idf_geometry(\n", + " overhang_building,\n", + " color_mode=\"surface_type\",\n", + ")" + ], + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "More advanced shading systems can be generated using `set_shading_object()`, which combines geometry creation and optical property assignment in a single call.\n", + "\n", + "In the following example, vertical louvers are generated in front of each window and assigned custom optical properties, including solar reflectance and transmittance. This approach is particularly useful for representing façade-integrated shading systems such as metal louvers, photovoltaic panels or vegetation screens." + ] + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "from energytool.modifier import set_shading_object\n", + "\n", + "shaded_building = deepcopy(building)\n", + "\n", + "set_shading_object(\n", + " model=shaded_building,\n", + " geometry={\n", + " \"Type\": \"vertical_louvers\",\n", + " \"Depth\": 0.15,\n", + " \"Spacing\": 0.25,\n", + " \"Tilt\" : 30\n", + " },\n", + " properties={\n", + " \"Diffuse_Solar_Reflectance_of_Unglazed_Part_of_Shading_Surface\": 0.15,\n", + " \"Transmittance\": 0.35,\n", + " },\n", + ")\n", + "\n", + "plot_idf_geometry(\n", + " shaded_building,\n", + " color_mode=\"zone\",\n", + ")" + ], + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "EnergyPlus also supports shading devices attached directly to glazing systems.\n", + "\n", + "\n", + "Unlike geometric shadings, these systems do not create explicit geometry. Instead, they modify the optical and thermal behaviour of the window itself.\n", + "\n", + "A `Shade` represents a continuous layer such as a textile screen, roller shade or blackout curtain." + ] + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "from energytool.modifier import set_shade\n", + "\n", + "shade_building = deepcopy(building)\n", + "\n", + "set_shade(\n", + " model=shade_building,\n", + " description={\n", + " \"Name\": \"INTERIOR_SHADE\",\n", + " \"Solar_Transmittance\": 0.1,\n", + " },\n", + ")\n", + "\n", + "shade_building.idf.idfobjects[\"WINDOWMATERIAL:SHADE\"]" + ], + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "A `Blind` represents a slatted shading device such as a venetian blind or an exterior brise-soleil orientable (BSO).\n", + "\n", + "In addition to optical properties, blinds include geometric parameters such as slat width, spacing and angle. Although these devices are not represented as explicit geometry in the EnergyPlus model, their optical behaviour is calculated using detailed blind models." + ] + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "from energytool.modifier import set_blind\n", + "\n", + "blind_building = deepcopy(building)\n", + "\n", + "set_blind(\n", + " model=blind_building,\n", + " description={\n", + " \"Preset\": \"bso_exterior\",\n", + " },\n", + ")\n", + "\n", + "blind_building.idf.idfobjects[\"WINDOWMATERIAL:BLIND\"]" + ], + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "Unlike geometric shading surfaces, shades and blinds are not visible in `plot_idf_geometry()`.\n", + "\n", + "Their presence can be verified by inspecting the generated `WindowMaterial:Shade`, `WindowMaterial:Blind` and `WindowShadingControl` objects in the IDF model." + ] + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "## Time control\n", + "\n", + "Shading devices can also be controlled dynamically using EnergyPlus schedules. This allows users to represent occupant behaviour, automated façade controls or seasonal operation strategies. In the following example, a shade is linked to a schedule named `SUMMER_SHADE`." + ] + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "from energytool.modifier import update_idf_objects\n", + "\n", + "update_idf_objects(\n", + " building,\n", + " {\n", + " \"SUMMER_SHADE\": {\n", + " \"Name\": \"SUMMER_SHADE\",\n", + " \"Schedule_Type_Limits_Name\": \"Fraction\",\n", + " \"Hourly_Value\": 1,\n", + " },\n", + " },\n", + " \"Schedule:Constant\",\n", + ")\n", + "\n", + "set_shade(\n", + " model=building,\n", + " description={\n", + " \"Name\": \"INTERIOR_SHADE\",\n", + " \"Schedule\": \"SUMMER_SHADE\",\n", + " },\n", + ")\n", + "\n", + "for obj in building.idf.idfobjects[\n", + " \"WINDOWSHADINGCONTROL\"\n", + "]:\n", + " print(obj.Name)\n", + " print(obj.Schedule_Name)" + ], + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "The shade is now controlled through an EnergyPlus schedule. This allows users to represent occupant behaviour or automated solar control strategies." + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "## Shading transmittance schedule\n", + "\n", + "Geometric shading surfaces can also be assigned a transmittance schedule. This is particularly useful for representing vegetation systems whose density varies throughout the year.\n", + "\n", + "In the example below, a simplified deciduous tree model is created: the shading system is highly transmissive during winter and significantly more opaque during summer." + ] + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "from energytool.modifier import set_shading_properties\n", + "\n", + "set_shading_properties(\n", + " model=shaded_building,\n", + " description={\n", + " \"Transmittance_Schedule\":\n", + " \"DECIDUOUS_TREE\"\n", + " }\n", + ")\n", + "\n", + "update_idf_objects(\n", + " shaded_building,\n", + " {\n", + " \"DECIDUOUS_TREE\": {\n", + " \"Name\": \"DECIDUOUS_TREE\",\n", + " \"Schedule_Type_Limits_Name\": \"Fraction\",\n", + "\n", + " \"Field_1\": \"Through: 03/31\",\n", + " \"Field_2\": \"For: AllDays\",\n", + " \"Field_3\": \"Until: 24:00\",\n", + " \"Field_4\": 0.8,\n", + "\n", + " \"Field_5\": \"Through: 10/31\",\n", + " \"Field_6\": \"For: AllDays\",\n", + " \"Field_7\": \"Until: 24:00\",\n", + " \"Field_8\": 0.3,\n", + "\n", + " \"Field_9\": \"Through: 12/31\",\n", + " \"Field_10\": \"For: AllDays\",\n", + " \"Field_11\": \"Until: 24:00\",\n", + " \"Field_12\": 0.8,\n", + " }\n", + " },\n", + " \"Schedule:Compact\",\n", + ")\n", + "\n", + "set_shading_properties(\n", + " model=shaded_building,\n", + " description={\n", + " \"Transmittance_Schedule\":\n", + " \"DECIDUOUS_TREE\"\n", + " }\n", + ")\n", + "\n", + "[\n", + " (\n", + " obj.Name,\n", + " obj.Transmittance_Schedule_Name\n", + " )\n", + " for obj in shaded_building.idf.idfobjects[\n", + " \"SHADING:ZONE:DETAILED\"\n", + " ]\n", + " if obj.Transmittance_Schedule_Name\n", + "][:5]" + ], + "outputs": [], + "execution_count": null + }, { "metadata": {}, "cell_type": "code",