diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 48235efe4..0c5284186 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -36,7 +36,7 @@ repos: - id: pydocstyle additional_dependencies: [toml] exclude: tests/|examples/ - + - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.4.0 hooks: diff --git a/doc/source/methods/MotorCAD_object.rst b/doc/source/methods/MotorCAD_object.rst index 492a1594e..032cb2f05 100644 --- a/doc/source/methods/MotorCAD_object.rst +++ b/doc/source/methods/MotorCAD_object.rst @@ -19,6 +19,7 @@ MotorCAD API _autogen_FEA Geometry _autogen_General _autogen_Geometry + _autogen_Adaptive Geometry _autogen_Graphs _autogen_Internal Scripting _autogen_Lab diff --git a/doc/source/methods/autofill_function_names.py b/doc/source/methods/autofill_function_names.py index 3291c0a91..256b9bf25 100644 --- a/doc/source/methods/autofill_function_names.py +++ b/doc/source/methods/autofill_function_names.py @@ -14,6 +14,7 @@ def generate_method_docs(): "FEA Geometry", "General", "Geometry", + "Adaptive Geometry", "Graphs", "Internal Scripting", "Lab", @@ -29,6 +30,7 @@ def generate_method_docs(): "rpc_methods_fea_geometry.py", "rpc_methods_general.py", "rpc_methods_geometry.py", + "adaptive_geometry.py", "rpc_methods_graphs.py", "rpc_methods_internal_scripting.py", "rpc_methods_lab.py", diff --git a/doc/source/methods/geometry_functions.rst b/doc/source/methods/geometry_functions.rst new file mode 100644 index 000000000..6ec6a6265 --- /dev/null +++ b/doc/source/methods/geometry_functions.rst @@ -0,0 +1,22 @@ +.. currentmodule:: ansys.motorcad.core.geometry +Adaptive Geometry +================= +Add some info here about adaptive geometry + + +Geometry Objects +---------------- +.. autosummary:: + :toctree: _autosummary_geometry_methods + + Region + Line + Arc + +Geometry Functions +------------------ +.. autosummary:: + :toctree: _autosummary_geometry_functions + + xy_to_rt + rt_to_xy \ No newline at end of file diff --git a/doc/source/methods/index.rst b/doc/source/methods/index.rst index 21a4e5855..b248cc760 100644 --- a/doc/source/methods/index.rst +++ b/doc/source/methods/index.rst @@ -4,14 +4,14 @@ API reference ============= Motor-CAD API ------------- +------------- The ``MotorCAD`` object is used by default for PyMotorCAD scripting. For descriptions of this object's single class and its many methods, see :ref:`ref_MotorCAD_object`. Motor-CAD compatibility API --------------------------- +--------------------------- The ``MotorCADCompatibility`` object is used for running old ActiveX scripts. For information on backwards compatibility, see @@ -29,3 +29,4 @@ object, its single class, and its many methods, see MotorCAD_object MotorCADCompatibility_object + geometry_functions diff --git a/src/ansys/motorcad/core/geometry.py b/src/ansys/motorcad/core/geometry.py index b9bbc578b..6fd0df422 100644 --- a/src/ansys/motorcad/core/geometry.py +++ b/src/ansys/motorcad/core/geometry.py @@ -1,6 +1,518 @@ """Function for ``Motor-CAD geometry`` not attached to Motor-CAD instance.""" from cmath import polar, rect -from math import degrees, radians +from math import atan2, cos, degrees, pow, radians, sin, sqrt + + +class Region: + """Python representation of Motor-CAD geometry region.""" + + def __init__(self): + """Create geometry region and set parameters to defaults.""" + self.name = "" + self.material = "air" + self.colour = (0, 0, 0) + self.area = 0.0 + self.centroid = Coordinate(0, 0) + self.region_coordinate = Coordinate(0, 0) + self.duplications = 1 + self.entities = [] + + # expect other properties to be implemented here including number duplications, material etc + + def __eq__(self, other): + """Override the default equals implementation for Region.""" + if ( + isinstance(other, Region) + and self.name == other.name + and self.colour == other.colour + and self.area == other.area + and self.centroid == other.centroid + and self.region_coordinate == other.region_coordinate + and self.duplications == other.duplications + and self.entities == other.entities + ): + return True + else: + return False + + def add_entity(self, entity): + """Add entity to list of region entities. + + Parameters + ---------- + entity : Line or Arc + Line/arc entity class instance + """ + self.entities.append(entity) + + def insert_entity(self, index, entity): + """Insert entity to list of region entities at given index. + + Parameters + ---------- + index : int + Index of which to insert at + entity : Line or Arc + Line/arc entity class instance + """ + self.entities.insert(index, entity) + + def insert_polyline(self, index, polyline): + """Insert polyline at given index, polyline can be made up of line/arc entities. + + Parameters + ---------- + index : int + Index of which to insert at + polyline : list of Line or list of Arc + list of Line or list of Arc + """ + for count, entity in enumerate(polyline): + self.insert_entity(index + count, entity) + + def remove_entity(self, entity_remove): + """Remove the entity from the region. + + Parameters + ---------- + entity_remove : Line or Arc + Line/arc entity class instance + """ + for entity in self.entities: + if (entity.start == entity_remove.start) & (entity.end == entity_remove.end): + if type(entity) == Line: + self.entities.remove(entity) + elif type(entity) == Arc: + if (entity.centre == entity_remove.centre) & ( + entity.radius == entity_remove.radius + ): + self.entities.remove(entity) + + # method to receive region from Motor-CAD and create python object + def _from_json(self, json): + """Convert class from json object. + + Parameters + ---------- + json: dict + Represents geometry region + """ + # self.Entities = json.Entities + self.name = json["name"] + self.material = json["material"] + + self.colour = (json["colour"]["r"], json["colour"]["g"], json["colour"]["b"]) + self.area = json["area"] + + self.centroid = Coordinate(json["centroid"]["x"], json["centroid"]["y"]) + self.region_coordinate = Coordinate( + json["region_coordinate"]["x"], json["region_coordinate"]["y"] + ) + self.duplications = json["duplications"] + self.entities = _convert_entities_from_json(json["entities"]) + + # method to convert python object to send to Motor-CAD + def _to_json(self): + """Convert from Python class to Json object. + + Returns + ---------- + dict + Geometry region json representation + """ + region_dict = { + "name": self.name, + "material": self.material, + "colour": {"r": self.colour[0], "g": self.colour[1], "b": self.colour[2]}, + "area": self.area, + "centroid": {"x": self.centroid.x, "y": self.centroid.y}, + "region_coordinate": {"x": self.region_coordinate.x, "y": self.region_coordinate.y}, + "duplications": self.duplications, + "entities": _convert_entities_to_json(self.entities), + } + + 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 + + +class Coordinate: + """Python representation of coordinate in two-dimensional space. + + Parameters + ---------- + x : float + X value. + + y : float + Y value. + """ + + def __init__(self, x, y): + """Initialise Coordinate.""" + self.x = x + self.y = y + + def __eq__(self, other): + """Override the default equals implementation for Coordinate.""" + if isinstance(other, Coordinate) and self.x == other.x and self.y == other.y: + return True + else: + return False + + +class Entity: + """Generic parent class for geometric entities based upon a start and end coordinate. + + Parameters + ---------- + start : Coordinate + Start coordinate. + + end : Coordinate + End coordinate. + """ + + def __init__(self, start, end): + """Initialise Entity.""" + self.start = start + self.end = end + + def __eq__(self, other): + """Override the default equals implementation for Entity.""" + if isinstance(other, Entity) and self.start == other.start and self.end == other.end: + return True + else: + return False + + +class Line(Entity): + """Python representation of Motor-CAD line entity based upon start and end coordinates. + + Parameters + ---------- + start : Coordinate + Start coordinate. + + end : Coordinate + End coordinate. + """ + + def __init__(self, start, end): + """Initialise Line.""" + super().__init__(start, end) + + def __eq__(self, other): + """Override the default equals implementation for Line.""" + if isinstance(other, Line) and self.start == other.start and self.end == other.end: + return True + else: + return False + + def get_coordinate_from_percentage_distance(self, ref_coordinate, percentage): + """Get the coordinate at the percentage distance along the line from the reference. + + Parameters + ---------- + ref_coordinate : Coordinate + Entity reference coordinate. + + percentage : float + Percentage distance along Line. + + Returns + ------- + Coordinate + Coordinate at percentage distance along Line. + """ + if ref_coordinate == self.end: + coordinate_1 = self.end + coordinate_2 = self.start + else: + coordinate_1 = self.start + coordinate_2 = self.end + + length = self.get_length() + + t = (length * percentage) / length + x = ((1 - t) * coordinate_1.x) + (t * coordinate_2.x) + y = ((1 - t) * coordinate_1.y) + (t * coordinate_2.y) + + return Coordinate(x, y) + + def get_coordinate_from_distance(self, ref_coordinate, distance): + """Get the coordinate at the specified distance along the line from the reference. + + Parameters + ---------- + ref_coordinate : Coordinate + Entity reference coordinate. + + distance : float + Distance along Line. + + Returns + ------- + Coordinate + Coordinate at distance along Line. + """ + if ref_coordinate == self.end: + coordinate_1 = self.end + coordinate_2 = self.start + else: + coordinate_1 = self.start + coordinate_2 = self.end + + t = distance / self.get_length() + x = ((1 - t) * coordinate_1.x) + (t * coordinate_2.x) + y = ((1 - t) * coordinate_1.y) + (t * coordinate_2.y) + + return Coordinate(x, y) + + def get_length(self): + """Get length of line. + + Returns + ------- + float + Length of line + """ + return sqrt(pow(self.start.x - self.end.x, 2) + pow(self.start.y - self.end.y, 2)) + + +class Arc(Entity): + """Python representation of Motor-CAD arc entity based upon start, end, centre and radius. + + Parameters + ---------- + start : Coordinate + Start coordinate. + + end : Coordinate + End coordinate. + + centre :Coordinate + Centre coordinate. + + radius : float + Arc radius + """ + + def __init__(self, start, end, centre, radius): + """Initialise Arc.""" + super().__init__(start, end) + self.radius = radius + self.centre = centre + + def __eq__(self, other): + """Override the default equals implementation for Arc.""" + if ( + isinstance(other, Arc) + and self.start == other.start + and self.end == other.end + and self.centre == other.centre + and self.radius == other.radius + ): + return True + else: + return False + + def get_coordinate_from_percentage_distance(self, ref_coordinate, percentage): + """Get the coordinate at the percentage distance along the arc from the reference coord. + + Parameters + ---------- + ref_coordinate : Coordinate + Entity reference coordinate. + + percentage : float + Percentage distance along Arc. + + Returns + ------- + Coordinate + Coordinate at percentage distance along Arc. + """ + length = self.get_length() * percentage + + return self.get_coordinate_from_distance(ref_coordinate, length) + + def get_coordinate_from_distance(self, ref_coordinate, distance): + """Get the coordinate at the specified distance along the arc from the reference coordinate. + + Parameters + ---------- + ref_coordinate : Coordinate + Entity reference coordinate. + + distance : float + Distance along arc. + + Returns + ------- + Coordinate + Coordinate at distance along Arc. + """ + if ref_coordinate == self.end: + if self.radius >= 0: + # anticlockwise + angle = atan2(ref_coordinate.y, ref_coordinate.x) - (distance / self.radius) + else: + angle = atan2(ref_coordinate.y, ref_coordinate.x) + (distance / self.radius) + else: + if self.radius >= 0: + # anticlockwise + angle = atan2(ref_coordinate.y, ref_coordinate.x) + (distance / self.radius) + else: + angle = atan2(ref_coordinate.y, ref_coordinate.x) - (distance / self.radius) + + return Coordinate( + self.centre.x + self.radius * cos(angle), self.centre.y + self.radius * sin(angle) + ) + + def get_length(self): + """Get length of arc from start to end along circumference. + + Returns + ------- + float + Length of arc + """ + radius, angle_1 = xy_to_rt(self.start.x, self.start.y) + radius, angle_2 = xy_to_rt(self.end.x, self.end.y) + + if self.radius == 0: + arc_angle = 0 + elif ((self.radius > 0) and (angle_1 > angle_2)) or ( + (self.radius < 0) and angle_2 < angle_1 + ): + arc_angle = angle_2 - (angle_1 - 360) + else: + arc_angle = angle_2 - angle_1 + + return self.radius * radians(arc_angle) + + +def _convert_entities_to_json(entities): + """Get entities list as a json object. + + Parameters + ---------- + entities : list of Line or list of Arc + List of Line/Arc class objects representing entities. + + Returns + ------- + dict + entities in json format + """ + json_entities = [] + + for entity in entities: + if isinstance(entity, Line): + json_entities.append( + { + "type": "line", + "start": {"x": entity.start.x, "y": entity.start.y}, + "end": {"x": entity.end.x, "y": entity.end.y}, + } + ) + elif isinstance(entity, Arc): + json_entities.append( + { + "type": "arc", + "start": {"x": entity.start.x, "y": entity.start.y}, + "end": {"x": entity.end.x, "y": entity.end.y}, + "centre": {"x": entity.centre.x, "y": entity.centre.y}, + "radius": entity.radius, + } + ) + + return json_entities + + +def _convert_entities_from_json(json_array): + """Convert entities from json object to list of Arc/Line class. + + Parameters + ---------- + json_array : Json object + Array of Json objects representing lines/arcs. + + Returns + ------- + list of Line or list of Arc + list of Line and Arc objects + """ + entities = [] + + for entity in json_array: + if entity["type"] == "line": + entities.append( + Line( + Coordinate(entity["start"]["x"], entity["start"]["y"]), + Coordinate(entity["end"]["x"], entity["end"]["y"]), + ) + ) + elif entity["type"] == "arc": + entities.append( + Arc( + Coordinate(entity["start"]["x"], entity["start"]["y"]), + Coordinate(entity["end"]["x"], entity["end"]["y"]), + Coordinate(entity["centre"]["x"], entity["centre"]["y"]), + entity["radius"], + ) + ) + + return entities + + +def get_entities_have_common_coordinate(entity_1, entity_2): + """Check whether region entities create a closed region. + + Parameters + ---------- + entity_1 : Line or Arc + Line or Arc object to check for common coordinate + + entity_2 : Line or Arc + Line or Arc object to check for common coordinate + + Returns + ---------- + Boolean + """ + if ( + (entity_1.end == entity_2.start) + or (entity_1.end == entity_2.end) + or (entity_1.start == entity_2.start) + or (entity_1.start == entity_2.end) + ): + # found common coordinate between first and last entities + return True + else: + return False def xy_to_rt(x, y): diff --git a/src/ansys/motorcad/core/methods/adaptive_geometry.py b/src/ansys/motorcad/core/methods/adaptive_geometry.py new file mode 100644 index 000000000..db8437985 --- /dev/null +++ b/src/ansys/motorcad/core/methods/adaptive_geometry.py @@ -0,0 +1,132 @@ +"""Methods for adaptive geometry.""" +from ansys.motorcad.core.geometry import Region + + +class _RpcMethodsAdaptiveGeometry: + def __init__(self, mc_connection): + self.connection = mc_connection + + def set_adaptive_parameter_value(self, name, value): + """Set adaptive parameter, if parameter does not exist then add it. + + Parameters + ---------- + name : string + name of parameter. + value : float + value of parameter. + """ + self.connection.ensure_version_at_least("2024.0") + method = "SetAdaptiveParameterValue" + params = [name, value] + return self.connection.send_and_receive(method, params) + + def get_adaptive_parameter_value(self, name): + """Get adaptive parameter. + + Parameters + ---------- + name : string + name of parameter. + + Returns + ------- + float + value of parameter. + """ + self.connection.ensure_version_at_least("2024.0") + method = "GetAdaptiveParameterValue" + params = [name] + return self.connection.send_and_receive(method, params) + + def get_region(self, name): + """Get Motor-CAD geometry region. + + Parameters + ---------- + name : string + name of region. + + Returns + ------- + ansys.motorcad.core.geometry.Region + Motor-CAD region object. + + """ + self.connection.ensure_version_at_least("2024.0") + method = "GetRegion" + params = [name] + raw_region = self.connection.send_and_receive(method, params) + + region = Region() + region._from_json(raw_region) + + return region + + def set_region(self, region): + """Set Motor-CAD geometry region. + + Parameters + ---------- + region : ansys.motorcad.core.geometry.Region + Motor-CAD region object. + """ + self.connection.ensure_version_at_least("2024.0") + raw_region = region._to_json() + + method = "SetRegion" + params = [raw_region] + return self.connection.send_and_receive(method, params) + + def check_closed_region(self, region): + """Check region is closed using region detection. + + Parameters + ---------- + region : ansys.motorcad.core.geometry.Region + Motor-CAD region object. + """ + self.connection.ensure_version_at_least("2024.0") + pass + + def check_collisions(self, region, regions_to_check): + """Check region does not collide with other geometry regions. + + Parameters + ---------- + region : ansys.motorcad.core.geometry.Region + Motor-CAD region object. + + regions_to_check : list of ansys.motorcad.core.geometry.Region + list of Motor-CAD region object + """ + self.connection.ensure_version_at_least("2024.0") + raw_region = region._to_json() + raw_regions = [region_to_Check._to_json() for region_to_Check in regions_to_check] + + method = "Check_Collisions" + params = [raw_region, raw_regions] + + raw_collision_regions = self.connection.send_and_receive(method, params) + + collision_regions = [] + + for raw_collision_region in raw_collision_regions: + collision_region = Region() + collision_region._from_json(raw_collision_region) + collision_regions.append(collision_region) + + return collision_regions + + def save_adaptive_script(self, filepath): + """Save adaptive templates script file to Motor-CAD. + + Parameters + ---------- + filepath : string + full file path of script + """ + self.connection.ensure_version_at_least("2024.0") + method = "SaveAdaptiveScript" + params = [filepath] + return self.connection.send_and_receive(method, params) diff --git a/src/ansys/motorcad/core/rpc_methods_core.py b/src/ansys/motorcad/core/rpc_methods_core.py index ef959fdf6..bea731861 100644 --- a/src/ansys/motorcad/core/rpc_methods_core.py +++ b/src/ansys/motorcad/core/rpc_methods_core.py @@ -3,6 +3,7 @@ Not for direct use. Inherited by _MotorCADCore/_RpcMethodsCoreOld """ +from ansys.motorcad.core.methods.adaptive_geometry import _RpcMethodsAdaptiveGeometry from ansys.motorcad.core.methods.rpc_methods_calculations import _RpcMethodsCalculations from ansys.motorcad.core.methods.rpc_methods_fea_geometry import _RpcMethodsFEAGeometry from ansys.motorcad.core.methods.rpc_methods_general import _RpcMethodsGeneral @@ -28,6 +29,7 @@ class _RpcMethodsCore( _RpcMethodsInternalScripting, _RpcMethodsFEAGeometry, _RpcMethodsMaterials, + _RpcMethodsAdaptiveGeometry, ): def __init__(self, mc_connection): self.connection = mc_connection @@ -43,3 +45,4 @@ def __init__(self, mc_connection): _RpcMethodsInternalScripting.__init__(self, self.connection) _RpcMethodsFEAGeometry.__init__(self, self.connection) _RpcMethodsMaterials.__init__(self, self.connection) + _RpcMethodsAdaptiveGeometry.__init__(self, self.connection) diff --git a/tests/setup_test.py b/tests/setup_test.py index 35f26f00f..a27811ebc 100644 --- a/tests/setup_test.py +++ b/tests/setup_test.py @@ -30,6 +30,8 @@ def setup_test_env(): launch_new_motorcad = False + pymotorcad.rpc_client_core.DONT_CHECK_MOTORCAD_VERSION = True + try: mc except NameError: diff --git a/tests/test_files/adaptive_templates_script.py b/tests/test_files/adaptive_templates_script.py new file mode 100644 index 000000000..455e5e72e --- /dev/null +++ b/tests/test_files/adaptive_templates_script.py @@ -0,0 +1,13 @@ +# Need to import pymotorcad to access Motor-CAD +import ansys.motorcad.core as pymotorcad + +# Connect to Motor-CAD +mc = pymotorcad.MotorCAD(open_new_instance=False) + +# get default template region from Motor-CAD +region = mc.geometry.get_region("Stator") +# update material and colour +region.material = "M43" +region.colour = (220, 20, 60) +# set region back into Motor-CAD (updates the default stator region) +mc.geometry.set_region(region) diff --git a/tests/test_geometry.py b/tests/test_geometry.py index 8941593a2..109ac10f8 100644 --- a/tests/test_geometry.py +++ b/tests/test_geometry.py @@ -1,12 +1,33 @@ +import math +from math import isclose, sqrt + import pytest -from ansys.motorcad.core import MotorCADError +from RPC_Test_Common import get_dir_path +from ansys.motorcad.core import MotorCADError, geometry from setup_test import setup_test_env # Get Motor-CAD exe mc = setup_test_env() +def generate_constant_region(): + region = geometry.Region() + region.name = "testing_region" + region.colour = (0, 0, 255) + region.material = "Air" + + 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 + ) + ) + region.entities.append(geometry.Line(geometry.Coordinate(1, 1), geometry.Coordinate(-1, 0))) + + return region + + def test_set_get_winding_coil(): phase = 1 path = 1 @@ -51,3 +72,309 @@ def test_check_if_geometry_is_valid(): mc.check_if_geometry_is_valid(1) mc.set_variable("Slot_Depth", save_slot_depth) + + +def test_set_adaptive_parameter_value(): + parameter_name = "test_parameter" + parameter_value = 100 + + mc.set_adaptive_parameter_value(parameter_name, parameter_value) + assert mc.get_array_variable("AdaptiveTemplates_Parameters_Name", 0) == parameter_name + assert mc.get_array_variable("AdaptiveTemplates_Parameters_Value", 0) == parameter_value + + parameter_value = 70 + # update existing parameter + mc.set_adaptive_parameter_value(parameter_name, parameter_value) + assert mc.get_array_variable("AdaptiveTemplates_Parameters_Name", 0) == parameter_name + assert mc.get_array_variable("AdaptiveTemplates_Parameters_Value", 0) == parameter_value + + +def test_set_adaptive_parameter_value_incorrect_type(): + with pytest.raises(MotorCADError): + mc.set_adaptive_parameter_value("incorrect_type", "test_string") + + +def test_get_adaptive_parameter_value(): + mc.set_adaptive_parameter_value("test_parameter_1", 100) + + value = mc.get_adaptive_parameter_value("test_parameter_1") + assert value == 100 + + +def test_get_adaptive_parameter_value_does_not_exist(): + with pytest.raises(Exception) as e_info: + mc.get_adaptive_parameter_value("testing_parameter") + + assert "No adaptive parameter found with name" in str(e_info.value) + + +def test_get_region(): + expected_region = generate_constant_region() + mc.set_region(expected_region) + + region = mc.get_region(expected_region.name) + assert region == expected_region + + with pytest.raises(Exception) as e_info: + mc.get_region("Rotor_Magnet") + + assert ("region" in str(e_info.value)) and ("name" in str(e_info.value)) + + +def test_set_region(): + region = generate_constant_region() + mc.set_region(region) + returned_region = mc.get_region("testing_region") + assert returned_region == region + + +def test_save_adaptive_script(): + filepath = get_dir_path() + r"\test_files\adaptive_templates_script.py" + mc.save_adaptive_script(filepath) + + num_lines = mc.get_variable("AdaptiveTemplates_ScriptLines") + + with open(filepath, "rbU") as f: + num_lines_file = sum(1 for _ in f) + + assert num_lines == num_lines_file + + +def test_region_add_entity_line(): + # generate entity to add to region + entity = geometry.Line(geometry.Coordinate(0, 0), geometry.Coordinate(1, 1)) + + expected_region = generate_constant_region() + expected_region.entities.append(entity) + + region = generate_constant_region() + region.add_entity(entity) + + assert region == expected_region + + +def test_region_add_entity_arc(): + # generate entity to add to region + entity = geometry.Arc( + geometry.Coordinate(-1, 0), geometry.Coordinate(1, 0), geometry.Coordinate(0, 0), 1 + ) + + expected_region = generate_constant_region() + expected_region.entities.append(entity) + + region = generate_constant_region() + region.add_entity(entity) + + assert region == expected_region + + +def test_region_insert_entity(): + entity = geometry.Line(geometry.Coordinate(-2, 2), geometry.Coordinate(2, 3)) + + expected_region = generate_constant_region() + expected_region.entities.insert(1, entity) + + region = generate_constant_region() + region.insert_entity(1, entity) + + assert region == expected_region + + +def test_region_insert_polyline(): + polyline = [ + geometry.Line(geometry.Coordinate(0, 0), geometry.Coordinate(1, 1)), + geometry.Arc( + geometry.Coordinate(1, 1), geometry.Coordinate(1, 0), geometry.Coordinate(0, 0), 1 + ), + geometry.Line(geometry.Coordinate(1, 0), geometry.Coordinate(0, 0)), + ] + + expected_region = generate_constant_region() + expected_region.entities = polyline + expected_region.entities + + region = generate_constant_region() + region.insert_polyline(0, polyline) + + assert region == expected_region + + +def test_region_remove_entity(): + expected_region = generate_constant_region() + + entity = expected_region.entities[1] + expected_region.entities.remove(entity) + + region = generate_constant_region() + region.remove_entity(entity) + + assert region == expected_region + + +def test_region_from_json(): + raw_region = { + "name": "test_region", + "material": "copper", + "colour": {"r": 240, "g": 0, "b": 0}, + "area": 5.1, + "centroid": {"x": 0.0, "y": 1.0}, + "region_coordinate": {"x": 0.0, "y": 1.1}, + "duplications": 10, + "entities": [], + } + + test_region = geometry.Region() + test_region.name = "test_region" + test_region.material = "copper" + test_region.colour = (240, 0, 0) + test_region.area = 5.1 + test_region.centroid = geometry.Coordinate(0.0, 1.0) + test_region.region_coordinate = geometry.Coordinate(0.0, 1.1) + test_region.duplications = 10 + test_region.entities = [] + + region = geometry.Region() + region._from_json(raw_region) + + assert region == test_region + + +def test_region_to_json(): + raw_region = { + "name": "test_region", + "material": "copper", + "colour": {"r": 240, "g": 0, "b": 0}, + "area": 5.1, + "centroid": {"x": 0.0, "y": 1.0}, + "region_coordinate": {"x": 0.0, "y": 1.1}, + "duplications": 10, + "entities": [], + } + + test_region = geometry.Region() + test_region.name = "test_region" + test_region.material = "copper" + test_region.colour = (240, 0, 0) + test_region.area = 5.1 + test_region.centroid = geometry.Coordinate(0.0, 1.0) + test_region.region_coordinate = geometry.Coordinate(0.0, 1.1) + test_region.duplications = 10 + test_region.entities = [] + + assert test_region._to_json() == raw_region + + +def test_region_is_closed(): + region = generate_constant_region() + + assert region.is_closed() + + +def test_line_get_coordinate_from_percentage_distance(): + line = geometry.Line(geometry.Coordinate(0, 0), geometry.Coordinate(2, 0)) + + coord = line.get_coordinate_from_percentage_distance(geometry.Coordinate(0, 0), 0.5) + assert coord == geometry.Coordinate(1, 0) + + +def test_line_get_coordinate_from_distance(): + line = geometry.Line(geometry.Coordinate(0, 0), geometry.Coordinate(2, 0)) + + assert line.get_coordinate_from_distance(geometry.Coordinate(0, 0), 1) == geometry.Coordinate( + 1, 0 + ) + + +def test_line_get_length(): + line = geometry.Line(geometry.Coordinate(0, 0), geometry.Coordinate(1, 1)) + + assert line.get_length() == sqrt(2) + + +def test_arc_get_coordinate_from_percentage_distance(): + arc = geometry.Arc( + geometry.Coordinate(-1, 0), geometry.Coordinate(1, 0), geometry.Coordinate(0, 0), 1 + ) + + coord = arc.get_coordinate_from_percentage_distance(geometry.Coordinate(-1, 0), 0.5) + assert isclose(coord.x, 0, abs_tol=1e-12) + assert isclose(coord.y, -1, abs_tol=1e-12) + + +def test_arc_get_coordinate_from_distance(): + arc = geometry.Arc( + geometry.Coordinate(-1, 0), geometry.Coordinate(1, 0), geometry.Coordinate(0, 0), 1 + ) + + coord = arc.get_coordinate_from_distance(geometry.Coordinate(-1, 0), math.pi / 2) + assert math.isclose(coord.x, 0, abs_tol=1e-12) + assert math.isclose(coord.y, -1, abs_tol=1e-12) + + +def test_arc_get_length(): + arc = geometry.Arc( + geometry.Coordinate(-1, 0), geometry.Coordinate(1, 0), geometry.Coordinate(0, 0), 1 + ) + + assert arc.get_length() == math.pi + + +def test_convert_entities_to_json(): + raw_entities = [ + {"type": "line", "start": {"x": 0.0, "y": 0.0}, "end": {"x": -1.0, "y": 0}}, + { + "type": "arc", + "start": {"x": -1.0, "y": 0.0}, + "end": {"x": 1.0, "y": 0.0}, + "centre": {"x": 0, "y": 0}, + "radius": 1.0, + }, + ] + + test_entities = [ + geometry.Line(geometry.Coordinate(0.0, 0.0), geometry.Coordinate(-1.0, 0)), + geometry.Arc( + geometry.Coordinate(-1.0, 0.0), + geometry.Coordinate(1.0, 0.0), + geometry.Coordinate(0.0, 0.0), + 1.0, + ), + ] + + assert geometry._convert_entities_to_json(test_entities) == raw_entities + + +def test_convert_entities_from_json(): + raw_entities = [ + {"type": "line", "start": {"x": 0.0, "y": 0.0}, "end": {"x": -1.0, "y": 0}}, + { + "type": "arc", + "start": {"x": -1.0, "y": 0.0}, + "end": {"x": 1.0, "y": 0.0}, + "centre": {"x": 0, "y": 0}, + "radius": 1.0, + }, + ] + + test_entities = [ + geometry.Line(geometry.Coordinate(0.0, 0.0), geometry.Coordinate(-1.0, 0)), + geometry.Arc( + geometry.Coordinate(-1.0, 0.0), + geometry.Coordinate(1.0, 0.0), + geometry.Coordinate(0.0, 0.0), + 1.0, + ), + ] + + converted_entities = geometry._convert_entities_from_json(raw_entities) + assert isinstance(converted_entities[0], type(test_entities[0])) + assert converted_entities[0] == test_entities[0] + + assert isinstance(converted_entities[1], type(test_entities[1])) + assert converted_entities[1] == test_entities[1] + + +def test_get_entities_have_common_coordinate(): + entity_1 = geometry.Line(geometry.Coordinate(0, 0), geometry.Coordinate(1, 1)) + entity_2 = geometry.Line(geometry.Coordinate(1, 1), geometry.Coordinate(2, 2)) + + assert geometry.get_entities_have_common_coordinate(entity_1, entity_2)