From 636891455db213d04b90fed46029463985212acc Mon Sep 17 00:00:00 2001 From: matthewAnsys <107551269+matthewAnsys@users.noreply.github.com> Date: Tue, 19 Sep 2023 10:57:26 +0100 Subject: [PATCH] New unit tests for collision detection api (#173) * New unit tests for collision detection api * Added drawings of collision types to unit tests * Clockwise region used for collision detection * Fix for failing tests, added equivalency for entities independent of list index and direction * Review updates --------- Co-authored-by: JackDavies --- doc/source/methods/geometry_functions.rst | 5 + src/ansys/motorcad/core/geometry.py | 80 +++++++- tests/test_geometry.py | 212 +++++++++++++++++++++- 3 files changed, 292 insertions(+), 5 deletions(-) diff --git a/doc/source/methods/geometry_functions.rst b/doc/source/methods/geometry_functions.rst index 6ec6a6265..e4b8be72e 100644 --- a/doc/source/methods/geometry_functions.rst +++ b/doc/source/methods/geometry_functions.rst @@ -10,6 +10,8 @@ Geometry Objects :toctree: _autosummary_geometry_methods Region + Coordinate + Entity Line Arc @@ -18,5 +20,8 @@ Geometry Functions .. autosummary:: :toctree: _autosummary_geometry_functions + get_entities_have_common_coordinate + entities_same + reverse_entities xy_to_rt rt_to_xy \ No newline at end of file diff --git a/src/ansys/motorcad/core/geometry.py b/src/ansys/motorcad/core/geometry.py index ba1eda1d9..67932bda2 100644 --- a/src/ansys/motorcad/core/geometry.py +++ b/src/ansys/motorcad/core/geometry.py @@ -27,10 +27,15 @@ def __eq__(self, other): and self.colour == other.colour # and self.area == other.area -> # Already check entities - can't expect user to calculate area - and self.centroid == other.centroid - and self.region_coordinate == other.region_coordinate + # and self.centroid == other.centroid -> + # Centroid calculated from entities - can't expect user to calculate + # and self.region_coordinate == other.region_coordinate -> + # Region coordinate is an output, cannot guarantee will be same for identical regions and self.duplications == other.duplications - and self.entities == other.entities + and ( + entities_same(self.entities, other.entities, check_reverse=False) + or entities_same(self.entities, other.entities, check_reverse=True) + ) ): return True else: @@ -207,6 +212,10 @@ def __eq__(self, other): else: return False + def reverse(self): + """Reverse Entity class.""" + return Entity(self.end, self.start) + class Line(Entity): """Python representation of Motor-CAD line entity based upon start and end coordinates. @@ -301,6 +310,10 @@ def get_length(self): """ return sqrt(pow(self.start.x - self.end.x, 2) + pow(self.start.y - self.end.y, 2)) + def reverse(self): + """Reverse Line entity.""" + return Line(self.end, self.start) + class Arc(Entity): """Python representation of Motor-CAD arc entity based upon start, end, centre and radius. @@ -414,6 +427,10 @@ def get_length(self): return self.radius * radians(arc_angle) + def reverse(self): + """Reverse Arc entity.""" + return Arc(self.end, self.start, self.centre, -self.radius) + def _convert_entities_to_json(entities): """Get entities list as a json object. @@ -516,6 +533,63 @@ def get_entities_have_common_coordinate(entity_1, entity_2): return False +def entities_same(entities_a, entities_b, check_reverse=False): + """Check whether entities in region are the same as entities a different region. + + Parameters + ---------- + entities_a : list of Line or list of Arc + list of Line and Arc objects. + + entities_b : list of Line or list of Arc + list of Line and Arc objects. + + check_reverse : Boolean + Whether to reverse entities when checking entity equivalency. + + Returns + ---------- + boolean + """ + if check_reverse: + entities_b = reverse_entities(entities_b) + + start_index = 0 + + for count, entity in enumerate(entities_b): + if entity == entities_a[0]: + # start entity found + start_index = count + break + + # regenerate entities_b from start index found from entities_a + entities = [entities_b[i] for i in range(start_index, len(entities_a))] + [ + entities_b[i] for i in range(0, start_index) + ] + + if entities == entities_a: + return True + else: + return False + + +def reverse_entities(entities): + """Reverse list of line/arc entities, including entity start end coordinates. + + Parameters + ---------- + entities : list of Line or list of Arc + list of Line and Arc objects. + + Returns + ---------- + list of Line or list of Arc + list of Line and Arc objects. + """ + entities.reverse() + return [entity.reverse() for entity in entities] + + def xy_to_rt(x, y): """Convert Motor-CAD Cartesian coordinates to polar coordinates in degrees. diff --git a/tests/test_geometry.py b/tests/test_geometry.py index 4031c0990..48f3ec232 100644 --- a/tests/test_geometry.py +++ b/tests/test_geometry.py @@ -20,10 +20,10 @@ def generate_constant_region(): region.entities.append(geometry.Line(geometry.Coordinate(-1, 0), geometry.Coordinate(1, 0))) region.entities.append( geometry.Arc( - geometry.Coordinate(1, 0), geometry.Coordinate(1, 1), geometry.Coordinate(0, 0), 1 + geometry.Coordinate(1, 0), geometry.Coordinate(0, 1), geometry.Coordinate(0, 0), 1 ) ) - region.entities.append(geometry.Line(geometry.Coordinate(1, 1), geometry.Coordinate(-1, 0))) + region.entities.append(geometry.Line(geometry.Coordinate(0, 1), geometry.Coordinate(-1, 0))) return region @@ -302,6 +302,73 @@ def test_region_is_closed(): assert region.is_closed() +def test_region_contains_same_entities(): + region = generate_constant_region() + + expected_region = region + expected_region.entities = geometry.reverse_entities(region.entities) + + assert region == expected_region + + +def test_reverse_entity(): + entity = geometry.Entity(geometry.Coordinate(0, 0), geometry.Coordinate(1, 1)) + expected_entity = geometry.Entity(geometry.Coordinate(1, 1), geometry.Coordinate(0, 0)) + + assert entity.reverse() == expected_entity + + +def test_reverse_line(): + region = generate_constant_region() + line = region.entities[0] + expected_line = geometry.Line(line.end, line.start) + + assert line.reverse() == expected_line + + +def test_reverse_arc(): + region = generate_constant_region() + arc = region.entities[1] + expected_line = geometry.Arc(arc.end, arc.start, arc.centre, -arc.radius) + + assert arc.reverse() == expected_line + + +def test_entities_same(): + region = generate_constant_region() + region_expected = generate_constant_region() + + assert geometry.entities_same(region.entities, region_expected.entities) + + +def test_entities_same_1(): + region = generate_constant_region() + + entities = [region.entities[i] for i in range(1, len(region.entities))] + [ + region.entities[i] for i in range(0, 1) + ] + + assert geometry.entities_same(region.entities, entities) + + +def test_reverse_entities(): + region = generate_constant_region() + + expected_entities = [] + + for entity in region.entities: + if isinstance(entity, geometry.Line): + expected_entities.append(geometry.Line(entity.end, entity.start)) + elif isinstance(entity, geometry.Arc): + expected_entities.append( + geometry.Arc(entity.end, entity.start, entity.centre, -entity.radius) + ) + + expected_entities.reverse() + + assert geometry.reverse_entities(region.entities) == expected_entities + + def test_line_get_coordinate_from_percentage_distance(): line = geometry.Line(geometry.Coordinate(0, 0), geometry.Coordinate(2, 0)) @@ -541,3 +608,144 @@ def test_unite_regions_2(): union = mc.unite_regions(square, [triangle]) assert expected_region == union + + +def test_check_collisions(): + """Collision Type : Collision detected. + No vertices from the other region within the other region.""" + # Before After + # + # |---| + # | | + # |--|---|--| + # | | | | -> Collision detected between regions + # | | | | + # |--|---|--| + # | | + # |---| + # + region_a = generate_constant_region() + + region_b = geometry.Region() + region_b.add_entity(geometry.Line(geometry.Coordinate(0, -2), geometry.Coordinate(1, 2))) + region_b.add_entity(geometry.Line(geometry.Coordinate(1, 2), geometry.Coordinate(5, -3))) + region_b.add_entity(geometry.Line(geometry.Coordinate(5, -3), geometry.Coordinate(0, -2))) + + collisions = mc.check_collisions(region_a, [region_b, mc.get_region("Stator")]) + num_collisions = len(collisions) + + assert num_collisions == 1 + assert collisions[0] == region_b + + +def test_check_collisions_1(): + """Collision Type : Collision Detected. + Two vertices from the other region within the other region.""" + # Before After + # + # |---------| + # | | -> Collision detected between regions + # | |---| | + # |--|---|--| + # | | + # |---| + # + region_a = generate_constant_region() + + region_b = geometry.Region() + region_b.add_entity( + geometry.Line(geometry.Coordinate(-0.2, -2), geometry.Coordinate(-0.2, 0.2)) + ) + region_b.add_entity( + geometry.Line(geometry.Coordinate(-0.2, 0.2), geometry.Coordinate(0.2, 0.2)) + ) + region_b.add_entity(geometry.Line(geometry.Coordinate(0.2, 0.2), geometry.Coordinate(0.2, -2))) + region_b.add_entity(geometry.Line(geometry.Coordinate(0.2, -2), geometry.Coordinate(-0.2, -2))) + + collisions = mc.check_collisions(region_a, [region_b, mc.get_region("Stator")]) + num_collisions = len(collisions) + + assert num_collisions == 1 + assert collisions[0] == region_b + + +def test_check_collisions_2(): + """Collision Type : No collision. + Regions touching on single entity""" + # Before After + # + # |---------| + # | | -> No collision detected between regions + # | | + # |--|---|--| + # | | + # |---| + # + region_a = generate_constant_region() + + region_b = geometry.Region() + region_b.add_entity(geometry.Line(geometry.Coordinate(-0.2, -2), geometry.Coordinate(-0.2, 0))) + region_b.add_entity(geometry.Line(geometry.Coordinate(-0.2, 0), geometry.Coordinate(0.2, 0))) + region_b.add_entity(geometry.Line(geometry.Coordinate(0.2, 0), geometry.Coordinate(0.2, -2))) + region_b.add_entity(geometry.Line(geometry.Coordinate(0.2, -2), geometry.Coordinate(-0.2, -2))) + + collisions = mc.check_collisions(region_a, [region_b, mc.get_region("Stator")]) + num_collisions = len(collisions) + + assert num_collisions == 0 + + +def test_check_collisions_3(): + """Collision Type : Collision detected. + No vertices from the other region within the other region. + Square region drawn clockwise.""" + # Before After + # + # |---| + # | | + # |--|---|--| + # | | | | -> Collision detected between regions + # | | | | + # |--|---|--| + # | | + # |---| + # + s_p = [] + s_l = [] + + s_p.append(geometry.Coordinate(0, 0)) + s_p.append(geometry.Coordinate(0, 2)) + s_p.append(geometry.Coordinate(2, 2)) + s_p.append(geometry.Coordinate(2, 0)) + + s_l.append(geometry.Line(s_p[0], s_p[1])) + s_l.append(geometry.Line(s_p[1], s_p[2])) + s_l.append(geometry.Line(s_p[2], s_p[3])) + s_l.append(geometry.Line(s_p[3], s_p[0])) + + square = geometry.Region() + for entity in s_l: + square.add_entity(entity) + + t_p = [] + t_l = [] + + t_p.append(geometry.Coordinate(1, 2.2)) + t_p.append(geometry.Coordinate(2.2, 1)) + t_p.append(geometry.Coordinate(4, 4)) + + t_l.append(geometry.Line(t_p[0], t_p[1])) + t_l.append(geometry.Line(t_p[1], t_p[2])) + t_l.append(geometry.Line(t_p[2], t_p[0])) + + triangle = geometry.Region() + for entity in t_l: + triangle.add_entity(entity) + + collisions = mc.check_collisions(triangle, [square]) + assert len(collisions) == 1 + assert collisions[0] == square + + collisions = mc.check_collisions(square, [triangle]) + assert len(collisions) == 1 + assert collisions[0] == triangle