-
Notifications
You must be signed in to change notification settings - Fork 5
Added new classes to return the edges of polyhedra #171
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
f0b8598
ab21279
22399d8
c9108cd
7c94100
0e6501a
fdec57c
dd47ed2
210f009
a2cfd3c
1f4a106
d387fca
6e69875
f473e27
cd67d45
01077cc
ada7029
7a89acb
b03099b
770199b
a879cc2
b4f60f9
6b72414
ce495d9
ced2fe6
20a8518
3a2f336
f730e0c
986fa8d
3c87b94
d3f5203
2a325e6
ec798c7
8d4b361
341cd53
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,6 +4,7 @@ | |
"""Defines a polyhedron.""" | ||
|
||
import warnings | ||
from functools import cached_property | ||
|
||
import numpy as np | ||
import rowan | ||
|
@@ -356,9 +357,45 @@ def vertices(self): | |
|
||
@property | ||
def faces(self): | ||
"""list(:class:`numpy.ndarray`): Get the polyhedron's faces.""" | ||
"""list(:class:`numpy.ndarray`): Get the polyhedron's faces. | ||
|
||
Results returned as vertex index lists. | ||
""" | ||
return self._faces | ||
|
||
@cached_property | ||
def edges(self): | ||
""":class:`numpy.ndarray`: Get the polyhedron's edges. | ||
|
||
Results returned as vertex index pairs, with each edge of the polyhedron | ||
included exactly once. Edge (i,j) pairs are ordered by vertex index with i<j. | ||
""" | ||
ij_pairs = np.array( | ||
[ | ||
[i, j] | ||
for face in self.faces | ||
for i, j in zip(face, np.roll(face, -1)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just making sure — the face indices are ordered consistently, right? In other words, this code relies on face indices being sorted in such a way that any shared edge is in the order (i, j) for one face and (j, i) for the other face sharing that edge. Is that a safe assumption from the rest of the code (it has been a while since I’ve read it). If this isn’t handled properly by the construction of the shape, then you’ll get missing edges. Are there any degenerate cases where this doesn’t happen automatically (can a polyhedron have one face in coxeter)? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am unsure exactly what the limits of Polyhedron are: for example, if self-intersecting shapes, shapes with holes, etc work properly. That being said, faces are ordered and edges are built using another method during initialization so it must work for the vast majority of use cases! I can take a deeper look in the next few days. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Complex polyhedra (within limits) seem to work - self intersecting polyhedra, polyhedra with holes, and simple non convex (e.g. stellated) polyhedra all compute edges as expected. However, there are cases where volumes, moments of inertia, and other properties are incorrect. There are some cases where edges does NOT work - but in these cases, many other methods break down. Polygons (one-face polyhedron), dihedra, and nonclosed solids tend to break this method but they also return null values for any method that includes volume. Some also have erroneous behavior in the centroid or inertia tensor calculation. I will create an issue for some of these edge cases in general, but overall the new edge methods are at least as robust as the other core methods. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good. It’s good to know the limitations — and which cases you are willing to treat as undefined behavior vs. raising an error or “fix” in some way. |
||
if i < j | ||
] | ||
) | ||
sorted_indices = np.lexsort(ij_pairs.T[::-1]) | ||
sorted_ij_pairs = ij_pairs[sorted_indices] | ||
# Make edge data read-only so that the cached property of this instance | ||
# cannot be edited | ||
sorted_ij_pairs.flags.writeable = False | ||
|
||
return sorted_ij_pairs | ||
|
||
@property | ||
def edge_vectors(self): | ||
""":class:`numpy.ndarray`: Get the polyhedron's edges as vectors.""" | ||
return self.vertices[self.edges[:, 1]] - self.vertices[self.edges[:, 0]] | ||
|
||
@property | ||
def num_edges(self): | ||
"""int: Get the number of edges.""" | ||
return len(self.edges) | ||
bdice marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
@property | ||
def volume(self): | ||
"""float: Get or set the polyhedron's volume.""" | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -28,7 +28,7 @@ | |
named_pyramiddipyramid_mark, | ||
sphere_isclose, | ||
) | ||
from coxeter.families import DOI_SHAPE_REPOSITORIES, PlatonicFamily | ||
from coxeter.families import DOI_SHAPE_REPOSITORIES, ArchimedeanFamily, PlatonicFamily | ||
from coxeter.shapes import ConvexPolyhedron, Polyhedron | ||
from coxeter.shapes.utils import rotate_order2_tensor, translate_inertia_tensor | ||
from utils import compute_centroid_mc, compute_inertia_mc | ||
|
@@ -339,6 +339,96 @@ def test___repr__(): | |
repr(icosidodecahedron) | ||
|
||
|
||
@combine_marks( | ||
named_platonic_mark, | ||
named_archimedean_mark, | ||
named_catalan_mark, | ||
named_johnson_mark, | ||
named_prismantiprism_mark, | ||
named_pyramiddipyramid_mark, | ||
) | ||
def test_edges(poly): | ||
# Check that the first column is in ascending order. | ||
assert np.all(np.diff(poly.edges[:, 0]) >= 0) | ||
|
||
# Check that all items in the first column are greater than those in the second. | ||
assert np.all(np.diff(poly.edges, axis=1) > 0) | ||
|
||
# Check the second column is in ascending order for each unique item in the first. | ||
# For example, [[0,1],[0,3],[1,2]] is permitted but [[0,1],[0,3],[0,2]] is not. | ||
edges = poly.edges | ||
unique_values = unique_values = np.unique(edges[:, 0]) | ||
assert all( | ||
[ | ||
np.all(np.diff(edges[edges[:, 0] == value, 1]) >= 0) | ||
for value in unique_values | ||
] | ||
) | ||
|
||
# Check that there are no duplicate edges. This also double-checks the sorting | ||
assert np.all(np.unique(poly.edges, axis=1) == poly.edges) | ||
|
||
# Check that the edges are immutable | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This can be simpler if you want. I would just use pytest’s “assert raises” context manager, wrapping the assignment. |
||
try: | ||
poly.edges[1] = [99, 99] | ||
# If the assignment works, catch that: | ||
assert poly.edges[1] != [99, 99] | ||
except ValueError as ve: | ||
assert "read-only" in str(ve) | ||
|
||
|
||
def test_edge_lengths(): | ||
known_shapes = { | ||
"Tetrahedron": np.sqrt(2) * np.cbrt(3), | ||
"Cube": 1, | ||
"Octahedron": np.power(2, 5 / 6) * np.cbrt(3 / 8), | ||
"Dodecahedron": np.power(2, 2 / 3) * np.cbrt(1 / (15 + np.sqrt(245))), | ||
"Icosahedron": np.cbrt(9 / 5 - 3 / 5 * np.sqrt(5)), | ||
} | ||
for name, edgelength in known_shapes.items(): | ||
poly = PlatonicFamily.get_shape(name) | ||
# Check that edge lengths are correct | ||
veclens = np.linalg.norm( | ||
poly.vertices[poly.edges[:, 1]] - poly.vertices[poly.edges[:, 0]], axis=1 | ||
) | ||
assert np.allclose(veclens, edgelength) | ||
assert np.allclose(veclens, np.linalg.norm(poly.edge_vectors, axis=1)) | ||
|
||
|
||
def test_num_edges_archimedean(): | ||
known_shapes = { | ||
"Cuboctahedron": 24, | ||
"Icosidodecahedron": 60, | ||
"Truncated Tetrahedron": 18, | ||
"Truncated Octahedron": 36, | ||
"Truncated Cube": 36, | ||
"Truncated Icosahedron": 90, | ||
"Truncated Dodecahedron": 90, | ||
"Rhombicuboctahedron": 48, | ||
"Rhombicosidodecahedron": 120, | ||
"Truncated Cuboctahedron": 72, | ||
"Truncated Icosidodecahedron": 180, | ||
"Snub Cuboctahedron": 60, | ||
"Snub Icosidodecahedron": 150, | ||
} | ||
for name, num_edges in known_shapes.items(): | ||
poly = ArchimedeanFamily.get_shape(name) | ||
assert poly.num_edges == num_edges | ||
|
||
|
||
@given( | ||
EllipsoidSurfaceStrategy, | ||
) | ||
def test_num_edges_polyhedron(points): | ||
hull = ConvexHull(points) | ||
poly = ConvexPolyhedron(points[hull.vertices]) | ||
ppoly = Polyhedron(poly.vertices, poly.faces) | ||
|
||
# Calculate correct number of edges from euler characteristic | ||
euler_characteristic_edge_count = ppoly.num_vertices + ppoly.num_faces - 2 | ||
assert ppoly.num_edges == euler_characteristic_edge_count | ||
|
||
|
||
def test_curvature(): | ||
"""Regression test against values computed with older method.""" | ||
# The shapes in the PlatonicFamily are normalized to unit volume. | ||
|
Uh oh!
There was an error while loading. Please reload this page.