Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 28 additions & 6 deletions energytool/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,8 @@ def plot_idf_geometry(
Display FenestrationSurface:Detailed objects.

show_shading_surfaces : bool, default=True
Display Shading:Zone:Detailed objects.
Display Shading:Zone:Detailed, Shading:Building:Detailed and
Shading:Site:Detailed objects.

show_names : bool, default=False
Display labels on the geometry.
Expand Down Expand Up @@ -238,7 +239,19 @@ def plot_idf_geometry(
_label_traces = []

def get_vertices(surface):
n_vertices = int(surface.Number_of_Vertices)
# "Number of Vertices" defaults to "autocalculate" in the E+ IDD and is
# frequently left blank by IDF generators (e.g. Honeybee/OpenStudio
# exports), so it can't always be relied upon: fall back to counting
# the actually populated Vertex_i_Xcoordinate fields in that case.
try:
n_vertices = int(surface.Number_of_Vertices)
except (TypeError, ValueError):
n_vertices = 0
while getattr(surface, f"Vertex_{n_vertices + 1}_Xcoordinate", "") not in (
"",
None,
):
n_vertices += 1

return np.array(
[
Expand Down Expand Up @@ -462,10 +475,19 @@ def get_building_surface_group(surface):
)

if show_shading_surfaces:
for surface in building.idf.idfobjects["SHADING:ZONE:DETAILED"]:
add_surface(
get_vertices(surface), "shading", "Shading", SHADING_COLOR, surface.Name,
)
# Zone-attached (e.g. overhangs, fins), building-attached (e.g. PV
# panels, balcony railings) and site-attached (e.g. neighbouring
# building masks) shading surfaces all share the same vertex-list
# schema.
for shading_key in (
"SHADING:ZONE:DETAILED",
"SHADING:BUILDING:DETAILED",
"SHADING:SITE:DETAILED",
):
for surface in building.idf.idfobjects[shading_key]:
add_surface(
get_vertices(surface), "shading", "Shading", SHADING_COLOR, surface.Name,
)

for key, group in groups.items():
fig.add_trace(
Expand Down
113 changes: 113 additions & 0 deletions tests/test_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -306,3 +306,116 @@ 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)


@pytest.fixture(scope="module")
def geo_building_blank_num_vertices():
"""Mimics IDF generators (e.g. Honeybee/OpenStudio) that commonly leave
"Number of Vertices" unset, relying on EnergyPlus's own autocalculation
rather than eppy's."""
try:
IDF.setiddname(RESOURCES_PATH / "Energy+.idd")
except eppy.modeleditor.IDDAlreadySetError:
pass

idf = IDF(StringIO(""))
idf.idfname = None

idf.newidfobject(key="Zone", Name="ConditionedZone")

def add_surface(obj_type, name, vertices, num_vertices_value, **attrs):
s = idf.newidfobject(key=obj_type, Name=name)
for k, v in attrs.items():
setattr(s, k, v)
if num_vertices_value is not None:
s.Number_of_Vertices = num_vertices_value
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

# left at the IDD default ("autocalculate"), as when the field is never touched
add_surface(
"BuildingSurface:Detailed", "ExtWall",
[(0, 0, 0), (3, 0, 0), (3, 0, 3), (0, 0, 3)],
None,
Surface_Type="Wall",
Outside_Boundary_Condition="Outdoors",
Zone_Name="ConditionedZone",
)
# explicitly blank, as found in some Honeybee-exported IDF text files
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 TestPlotIdfGeometryBlankNumVertices:
def test_does_not_raise_and_infers_vertex_count(self, geo_building_blank_num_vertices):
fig = tl.plot_idf_geometry(geo_building_blank_num_vertices)
assert isinstance(fig, go.Figure)

mesh_traces = [t for t in fig.data if isinstance(t, go.Mesh3d)]
# 4 vertices per surface (wall + shading), correctly inferred despite
# a missing/blank "Number of Vertices" field
assert len(mesh_traces) == 2
assert all(len(t.x) == 4 for t in mesh_traces)


@pytest.fixture(scope="module")
def geo_building_mixed_shading():
"""Honeybee/OpenStudio exports commonly represent context shading (PV
panels, balcony railings, neighbouring building masks) as
Shading:Building:Detailed / Shading:Site:Detailed rather than
Shading:Zone:Detailed."""
try:
IDF.setiddname(RESOURCES_PATH / "Energy+.idd")
except eppy.modeleditor.IDDAlreadySetError:
pass

idf = IDF(StringIO(""))
idf.idfname = None

def add_surface(obj_type, name, vertices):
s = idf.newidfobject(key=obj_type, Name=name)
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

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)],
)
add_surface(
"Shading:Building:Detailed", "PVPanel1",
[(0, -0.5, 1), (3, -0.5, 1), (3, -0.5, 2), (0, -0.5, 2)],
)
add_surface(
"Shading:Site:Detailed", "NeighbourMask1",
[(10, 0, 0), (10, 10, 0), (10, 10, 8), (10, 0, 8)],
)

return FakeBuilding(idf)


class TestPlotIdfGeometryMixedShading:
def test_all_shading_types_are_displayed(self, geo_building_mixed_shading):
fig = tl.plot_idf_geometry(geo_building_mixed_shading)

shading_traces = [t for t in fig.data if isinstance(t, go.Mesh3d) and t.name == "Shading"]
assert len(shading_traces) == 1

names = set(shading_traces[0].text)
assert names == {"Overhang1", "PVPanel1", "NeighbourMask1"}
assert len(shading_traces[0].x) == 12 # 3 surfaces x 4 vertices

def test_hide_shading_hides_all_types(self, geo_building_mixed_shading):
fig = tl.plot_idf_geometry(geo_building_mixed_shading, show_shading_surfaces=False)
assert not any(isinstance(t, go.Mesh3d) and t.name == "Shading" for t in fig.data)
Loading