Skip to content

New EntityList functions to construct and determine the properties of EntityLists #528

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

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
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
193 changes: 170 additions & 23 deletions src/ansys/motorcad/core/geometry.py
Original file line number Diff line number Diff line change
@@ -199,6 +199,23 @@ def insert_polyline(self, index, polyline):
for count, entity in enumerate(polyline):
self.insert_entity(index + count, entity)

def is_closed(self):
"""Check whether Region's entities are closed.

Returns
-------
Boolean
Whether Region is closed
"""
if len(self.entities) > 0:
# Make sure all adjacent entities have a point in common
return all(
get_entities_have_common_coordinate(self.entities[i - 1], self.entities[i])
for i in range(0, len(self.entities))
)
else:
return False

def remove_entity(self, entity_remove):
"""Remove the entity from the region.

@@ -324,29 +341,6 @@ def _to_json(self):

return region_dict

def is_closed(self):
"""Check whether region entities create a closed region.

Returns
-------
Boolean
Whether region is closed
"""
if len(self._entities) > 0:
entity_first = self._entities[0]
entity_last = self._entities[-1]

is_closed = get_entities_have_common_coordinate(entity_first, entity_last)

for i in range(len(self._entities) - 1):
is_closed = get_entities_have_common_coordinate(
self._entities[i], self._entities[i + 1]
)

return is_closed
else:
return False

@property
def parent_name(self):
"""Get or set the region parent name."""
@@ -2079,6 +2073,159 @@ def reverse(self):
for entity in self:
entity.reverse()

@classmethod
def polygon(cls, points, sort=False):
"""Create an EntityList from a list of points, connecting them with lines.

If sort is true, a centre point roughly in the centre of the supplied points will be
established and lines connecting the supplied points will be drawn anticlockwise about
that centre.

Parameters
----------
points : list of Coordinates or tuples
sort : bool
Reorders points to make geometry valid:
"""
# conversion to Coordinates:
if all(isinstance(point, tuple) for point in points):
points_new = [0] * len(points)
for i, p in enumerate(points):
points_new[i] = Coordinate(p[0], p[1])
points = points_new

if sort:
xcentre = sum(list(point.x for point in points)) / len(points)
ycentre = sum(list(point.y for point in points)) / len(points)

centre = Coordinate(xcentre, ycentre)

relative_points = list(deepcopy(point) for point in points)
for point in relative_points:
point.translate(-xcentre, -ycentre)

# To make sure behaviour is well-defined for points with an equal angular coordinate
# the points are first by sorted by radius, greatest to least
sorted_points_relative = sorted(
relative_points, key=lambda x: x.get_polar_coords_deg()[0], reverse=True
)

# Sort points by radial coordinate (relative to overall average position),
# from least to greatest angle (angles range from -180 to 180 degrees)
sorted_points_relative.sort(key=lambda x: x.get_polar_coords_deg()[1])

for point in sorted_points_relative:
point.translate(xcentre, ycentre)
points = sorted_points_relative

final_list = EntityList()

# Adds the lines
for count, point in enumerate(points):
if count == len(points) - 1:
final_list.append(Line(point, points[0]))
else:
final_list.append(Line(point, points[count + 1]))
if not final_list.has_valid_geometry:
warnings.warn("Entered point order may result in invalid geometry.")
return final_list

@property
def is_closed(self):
"""Check whether entities create a closed region.

Returns
-------
Boolean
Whether EntityList is closed
"""
if len(self) > 0:
# Make sure all adjacent entities have a point in common
return all(
get_entities_have_common_coordinate(self[i - 1], self[i])
for i in range(0, len(self))
)
else:
return False

@property
def self_intersecting(self):
"""Check whether entities intersect each other.

Returns
-------
Boolean
Whether entities are intersecting
"""
entity_pairs = []
# Create a set of all non-neighbor pairs of lines/arcs in the region
for entity in self:
for entity_other in self:
if entity != entity_other:
if (
entity.start != entity_other.end and entity.end != entity_other.start
) and not (
(entity, entity_other) in entity_pairs
or (entity_other, entity) in entity_pairs
):
entity_pairs.append((entity, entity_other))

# Check if they have any intersections
for entity1, entity2 in entity_pairs:
intersections = entity1.get_intersection(entity2)
if intersections is not None:
for intersection in intersections:
if entity1.coordinate_on_entity(intersection) and entity2.coordinate_on_entity(
intersection
):
return True
return False

@property
def is_anticlockwise(self):
"""Check whether EntityList is connected in an anticlockwise manner.

Returns
-------
Boolean
Whether EntityList is anticlockwise
"""
# Checks to make sure checking direction even makes sense
if not (not self.self_intersecting and self.is_closed):
raise Exception("Entities must be closed and nonintersecting")

# Find the lowest point, as well the entities coming in and out of that point
points = sorted(self.points, key=lambda p: p.y)
lowest = points[0]

for entity in self:
if entity.start == lowest:
ent_out = entity
if entity.end == lowest:
ent_in = entity

# Determine the angles of the entry and exit lines (arcs can make this tricky,
# so the angle is determined from the start and a point very near it)
exit_angle = Line(
ent_out.start, ent_out.get_coordinate_from_distance(ent_out.start, fraction=0.001)
).angle
entry_angle = Line(
ent_in.end, ent_in.get_coordinate_from_distance(ent_in.end, fraction=0.001)
).angle

return entry_angle > exit_angle

@property
def has_valid_geometry(self):
"""Check whether geometry is valid for motorcad.

Returns
-------
Boolean
Whether geometry is valid for motorcad
"""
return self.is_closed and (not self.self_intersecting) and self.is_anticlockwise

@property
def points(self):
"""Get points of shape/region from Entity list.
130 changes: 129 additions & 1 deletion tests/test_geometry.py
Original file line number Diff line number Diff line change
@@ -34,6 +34,7 @@
GEOM_TOLERANCE,
Arc,
Coordinate,
EntityList,
Line,
Region,
RegionMagnet,
@@ -420,10 +421,111 @@ def test_region_to_json():

def test_region_is_closed():
region = generate_constant_region()

assert region.is_closed()


def test_EntityList_is_closed():
region = generate_constant_region()
assert region.entities.is_closed

# Check an empty list returns as False
test_el1 = EntityList()
assert test_el1.is_closed == False

# Test an open polygon is not closed
test_el1.append(Line(Coordinate(0, 0), Coordinate(1, 1)))
test_el1.append(Line(Coordinate(1, 1), Coordinate(1, 0)))
test_el1.append(Line(Coordinate(1, 0), Coordinate(0.5, 0)))
assert test_el1.is_closed == False


def test_EntityList_self_intersecting():
# Test that intersection detection works properly on arcs
test_el1 = EntityList()
test_el1.append(Arc(Coordinate(0, 1), Coordinate(1, 0), centre=Coordinate(0.6, 0.6)))
test_el1.append(Line(Coordinate(0.4, 0.4), Coordinate(0, 1)))
test_el1.append(Line(Coordinate(1, 0), Coordinate(1, 1)))
test_el1.append(Line(Coordinate(1, 1), Coordinate(0.4, 0.4)))
assert test_el1.self_intersecting == False

# Test convex polygons are not detected as intersecting
test_el2 = EntityList()
test_el2.append(Line(Coordinate(0, 3), Coordinate(0, 0)))
test_el2.append(Line(Coordinate(0, 0), Coordinate(2, 0)))
test_el2.append(Line(Coordinate(2, 0), Coordinate(2, 1)))
test_el2.append(Line(Coordinate(2, 1), Coordinate(1, 1)))
test_el2.append(Line(Coordinate(1, 1), Coordinate(1, 2)))
test_el2.append(Line(Coordinate(1, 2), Coordinate(2, 2)))
test_el2.append(Line(Coordinate(2, 2), Coordinate(2, 3)))
test_el2.append(Line(Coordinate(2, 3), Coordinate(0, 3)))
assert test_el2.self_intersecting == False

# Test intersections are properly detected
test_el3 = EntityList()
test_el3.append(Line(Coordinate(0, 0), Coordinate(1, 1)))
test_el3.append(Line(Coordinate(1, 1), Coordinate(0, 1)))
test_el3.append(Line(Coordinate(0, 1), Coordinate(1, 0)))
test_el3.append(Line(Coordinate(1, 0), Coordinate(0, 0)))
assert test_el3.self_intersecting == True


def test_EntityList_is_anticlockwise():
assert generate_constant_region().entities.is_anticlockwise == True

# Test opposite winding
test_el2 = EntityList()
test_el2.append(Line(Coordinate(0, 0), Coordinate(0, 1)))
test_el2.append(Line(Coordinate(0, 1), Coordinate(1, 1)))
test_el2.append(Line(Coordinate(1, 1), Coordinate(1, 0)))
test_el2.append(Line(Coordinate(1, 0), Coordinate(0, 0)))
assert test_el2.is_anticlockwise == False

# Test that arcs with endpoints that would result in opposite ordering if connected with a line
# are correctly accounted for
test_el3 = EntityList()
test_el3.append(Line(Coordinate(0, 0), Coordinate(0, 2)))
test_el3.append(
Arc.from_coordinates(Coordinate(0.2, 2.4), Coordinate(-0.3, 1), Coordinate(0, 0))
)
test_el3.append(Line(Coordinate(0, 2), Coordinate(0.2, 2.4)))
assert test_el3.is_anticlockwise == True

# Test error detection
with pytest.raises(Exception, match="Entities must be closed and nonintersecting"):
test_el4 = EntityList()
test_el4.append(Line(Coordinate(0, 0), Coordinate(1, 1)))
test_el4.append(Line(Coordinate(1, 1), Coordinate(0, 1)))
test_el4.append(Line(Coordinate(0, 1), Coordinate(1, 0)))
test_el4.append(Line(Coordinate(1, 0), Coordinate(0, 0)))
test_el4.is_anticlockwise


def test_EntityList_has_valid_geometry():
assert generate_constant_region().entities.has_valid_geometry == True

# Test self_intersecting
test_el1 = EntityList()
test_el1.append(Line(Coordinate(0, 0), Coordinate(1, 1)))
test_el1.append(Line(Coordinate(1, 1), Coordinate(0, 1)))
test_el1.append(Line(Coordinate(0, 1), Coordinate(1, 0)))
test_el1.append(Line(Coordinate(1, 0), Coordinate(0, 0)))
assert test_el1.has_valid_geometry == False

# Test is_anticlockwise
test_el2 = EntityList()
test_el2.append(Line(Coordinate(0, 0), Coordinate(1, 1)))
test_el2.append(Line(Coordinate(1, 1), Coordinate(1, 0)))
test_el2.append(Line(Coordinate(1, 0), Coordinate(0, 0)))
assert test_el2.has_valid_geometry == False

# Test is_closed
test_el3 = EntityList()
test_el3.append(Line(Coordinate(0, 0), Coordinate(1, 1)))
test_el3.append(Line(Coordinate(1, 1), Coordinate(1, 0)))
test_el3.append(Line(Coordinate(1, 0), Coordinate(0.5, 0)))
assert test_el3.has_valid_geometry == False


def test_set_linked_region():
region = generate_constant_region()

@@ -769,6 +871,32 @@ def test_arc_length():
assert arc_2.length > line_1.length


def test_entities_polygon():
expected_square = create_square()

# test functionality without reordering
reg1 = Region(region_type=RegionType.stator)
reg1.entities = EntityList.polygon([(0, 0), (2, 0), (2, 2), (0, 2)])
assert expected_square == reg1

reg2 = Region(region_type=RegionType.stator)
reg2.entities = EntityList.polygon(
[Coordinate(0, 0), Coordinate(2, 0), Coordinate(2, 2), Coordinate(0, 2)]
)
assert expected_square == reg2

# Test warnings given when order is incorrect
with pytest.warns(UserWarning) as record:
reg3 = Region(region_type=RegionType.stator)
reg3.entities = EntityList.polygon([(0, 2), (2, 2), (2, 0), (0, 0)])
assert "Entered point order may result in invalid geometry." == record[0].message.args[0]

# test reordering
reg4 = Region(region_type=RegionType.stator)
reg4.entities = EntityList.polygon([(0, 0), (2, 2), (0, 2), (2, 0)], sort=True)
assert expected_square == reg4


def test_convert_entities_to_json():
raw_entities = [
{"type": "line", "start": {"x": 0.0, "y": 0.0}, "end": {"x": -1.0, "y": 0}},