From f3c5fbbacc75221b76e88e59a2761b20c442b120 Mon Sep 17 00:00:00 2001 From: mnagel Date: Wed, 18 Dec 2019 17:10:27 +0100 Subject: [PATCH 001/177] initial commit --- mypackage/mark_funcs.py | 32 ++++++++++++++++++++++++++++++++ tests/test_mark_funcs.py | 8 ++++++++ 2 files changed, 40 insertions(+) create mode 100644 mypackage/mark_funcs.py create mode 100644 tests/test_mark_funcs.py diff --git a/mypackage/mark_funcs.py b/mypackage/mark_funcs.py new file mode 100644 index 0000000..74094ba --- /dev/null +++ b/mypackage/mark_funcs.py @@ -0,0 +1,32 @@ +""" +Test Functions by Markus. +""" +import xarray as xr +import numpy as np + + +def function_one(): + """ + Print "Hello World"! + + :param: --- + :return: --- + """ + print("Hello World") + +def function_two(): + """ + Some stuff with Xarrays + + :param: --- + :return: --- + """ + xcoords=np.linspace(0,1,2) + ycoords=np.linspace(0,1,2) + a = xr.DataArray( + [[True, False],[False,True]], + name=["xcoords", "ycoords"], + dims=["x","y"], + coords=dict(x=xcoords, y=ycoords), + ) + a.sel(x=1, y=1, method="nearest", tolerance=0) \ No newline at end of file diff --git a/tests/test_mark_funcs.py b/tests/test_mark_funcs.py new file mode 100644 index 0000000..66336a0 --- /dev/null +++ b/tests/test_mark_funcs.py @@ -0,0 +1,8 @@ +import mypackage.mark_funcs as mf +import pytest + +def test_function_one(): + mf.function_one() + +def test_function_two(): + mf.function_two() \ No newline at end of file From b5e1c96db2c185c172646bdd68cec64e5614b201 Mon Sep 17 00:00:00 2001 From: mnagel Date: Wed, 18 Dec 2019 17:39:37 +0100 Subject: [PATCH 002/177] Xarray Anwendung mit Koordinaten --- mypackage/mark_funcs.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/mypackage/mark_funcs.py b/mypackage/mark_funcs.py index 74094ba..1f76d66 100644 --- a/mypackage/mark_funcs.py +++ b/mypackage/mark_funcs.py @@ -29,4 +29,11 @@ def function_two(): dims=["x","y"], coords=dict(x=xcoords, y=ycoords), ) - a.sel(x=1, y=1, method="nearest", tolerance=0) \ No newline at end of file + try: + if a.sel(x=1, y=1, method="nearest", tolerance=0): + raise KeyError + else: + a.loc[dict(x=1, y=1)] = True + + except KeyError: + print("Punkt schon vorhanden!") \ No newline at end of file From c2200354aa401a9622f88df66dd5429f96bf9179 Mon Sep 17 00:00:00 2001 From: mnagel Date: Wed, 18 Dec 2019 17:55:53 +0100 Subject: [PATCH 003/177] flake8 --- mypackage/mark_funcs.py | 16 ++++++++-------- tests/test_mark_funcs.py | 5 +++-- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/mypackage/mark_funcs.py b/mypackage/mark_funcs.py index 1f76d66..b7880b5 100644 --- a/mypackage/mark_funcs.py +++ b/mypackage/mark_funcs.py @@ -8,25 +8,26 @@ def function_one(): """ Print "Hello World"! - + :param: --- :return: --- """ print("Hello World") + def function_two(): """ Some stuff with Xarrays - + :param: --- :return: --- """ - xcoords=np.linspace(0,1,2) - ycoords=np.linspace(0,1,2) + xcoords = np.linspace(0, 1, 2) + ycoords = np.linspace(0, 1, 2) a = xr.DataArray( - [[True, False],[False,True]], + [[True, False], [False, True]], name=["xcoords", "ycoords"], - dims=["x","y"], + dims=["x", "y"], coords=dict(x=xcoords, y=ycoords), ) try: @@ -34,6 +35,5 @@ def function_two(): raise KeyError else: a.loc[dict(x=1, y=1)] = True - except KeyError: - print("Punkt schon vorhanden!") \ No newline at end of file + print("Punkt schon vorhanden!") diff --git a/tests/test_mark_funcs.py b/tests/test_mark_funcs.py index 66336a0..9d8a4ab 100644 --- a/tests/test_mark_funcs.py +++ b/tests/test_mark_funcs.py @@ -1,8 +1,9 @@ import mypackage.mark_funcs as mf -import pytest + def test_function_one(): mf.function_one() + def test_function_two(): - mf.function_two() \ No newline at end of file + mf.function_two() From 5ea3f185cf0c5c3c8dbfb5f2a7faf1de270ab39f Mon Sep 17 00:00:00 2001 From: mnagel Date: Wed, 18 Dec 2019 18:03:05 +0100 Subject: [PATCH 004/177] pydocstyle update --- mypackage/mark_funcs.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/mypackage/mark_funcs.py b/mypackage/mark_funcs.py index b7880b5..b97fa7f 100644 --- a/mypackage/mark_funcs.py +++ b/mypackage/mark_funcs.py @@ -1,13 +1,11 @@ -""" -Test Functions by Markus. -""" +"""Test Functions by Markus.""" import xarray as xr import numpy as np def function_one(): """ - Print "Hello World"! + Print "Hello World". :param: --- :return: --- @@ -17,7 +15,7 @@ def function_one(): def function_two(): """ - Some stuff with Xarrays + Working with Xarrays. :param: --- :return: --- From f72ef1fd09d59d5200a617ac05b601ab23cc346f Mon Sep 17 00:00:00 2001 From: mnagel Date: Wed, 18 Dec 2019 18:08:27 +0100 Subject: [PATCH 005/177] naming convention update --- mypackage/mark_funcs.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mypackage/mark_funcs.py b/mypackage/mark_funcs.py index b97fa7f..8ea440f 100644 --- a/mypackage/mark_funcs.py +++ b/mypackage/mark_funcs.py @@ -22,16 +22,16 @@ def function_two(): """ xcoords = np.linspace(0, 1, 2) ycoords = np.linspace(0, 1, 2) - a = xr.DataArray( + my_array = xr.DataArray( [[True, False], [False, True]], name=["xcoords", "ycoords"], dims=["x", "y"], coords=dict(x=xcoords, y=ycoords), ) try: - if a.sel(x=1, y=1, method="nearest", tolerance=0): + if my_array.sel(x=1, y=1, method="nearest", tolerance=0): raise KeyError else: - a.loc[dict(x=1, y=1)] = True + my_array.loc[dict(x=1, y=1)] = True except KeyError: print("Punkt schon vorhanden!") From c6c0871b0d3bef68d498d1e499a91847c4384867 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Fri, 20 Dec 2019 12:15:08 +0100 Subject: [PATCH 006/177] Add correct badge for docs --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d06f657..adeb4fb 100644 --- a/README.md +++ b/README.md @@ -13,4 +13,4 @@ #### Documention [![](https://github.com/vhirtham/pythonTest/workflows/pydocstyle/badge.svg)](https://github.com/vhirtham/pythonTest/actions?query=workflow%3A%22pydocstyle%22) -[![Documentation Status](https://readthedocs.org/projects/rtd-sphinx-theme-sample-project/badge/?version=latest)](https://rtd-sphinx-theme-sample-project.readthedocs.io/en/latest/?badge=latest) \ No newline at end of file +[![Documentation Status](https://readthedocs.org/projects/vhirthampythontest/badge/?version=latest)](https://vhirthampythontest.readthedocs.io/en/latest/?badge=latest) \ No newline at end of file From c54927ca9c664dfa0eb07885602d7bd3ce013341 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Thu, 2 Jan 2020 16:45:40 +0100 Subject: [PATCH 007/177] Add draft version of arc segment rasterization --- environment.yml | 2 +- mypackage/geometry.py | 80 +++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 79 insertions(+), 3 deletions(-) diff --git a/environment.yml b/environment.yml index de3ca1a..1cedc91 100644 --- a/environment.yml +++ b/environment.yml @@ -7,7 +7,7 @@ dependencies: - python=3.7 - numpy=1.17 - pandas=0.25 - - scipy=1.3 + - scipy=1.3.2 - xarray>=0.12.3 # - bottleneck>=1.1 # - jsonschema=2.6 diff --git a/mypackage/geometry.py b/mypackage/geometry.py index cc6b29c..e878bbe 100644 --- a/mypackage/geometry.py +++ b/mypackage/geometry.py @@ -1,6 +1,7 @@ """Provides classes to define lines and surfaces.""" import numpy as np +from scipy.spatial.transform import Rotation as R def check_point_data_valid(point): @@ -52,22 +53,39 @@ def check_valid(self, point_start, point_end): :return: --- """ + def rasterize(self, raster_width, point_start, point_end): + raise Exception("Function not defined for this segment type.") + class LineSegment(Segment): """Line segment.""" + def rasterize(self, raster_width, point_start, point_end): + length = np.linalg.norm(point_end - point_start) + num_raster_segments = np.round(length / raster_width) + nrw = 1. / num_raster_segments + + multiplier = np.arange(0, 1 - nrw / 2, nrw)[np.newaxis].transpose() + + raster_data = np.matmul((1 - multiplier), + point_start[np.newaxis]) + np.matmul( + multiplier, point_end[np.newaxis]) + + return raster_data + class ArcSegment(Segment): """Segment of a circle.""" - def __init__(self, point_center): + def __init__(self, center, winding_ccw=True): """ Constructor. :param point_center: Center point of the arc """ - point_center = np.array(point_center) + point_center = np.array(center) check_point_data_valid(point_center) self._point_center = point_center + self._winding_ccw = winding_ccw def check_valid(self, point_start, point_end): """ @@ -91,6 +109,49 @@ def check_valid(self, point_start, point_end): "Segment start and end points are not compatible with " "given center of the arc.") + def rasterize(self, raster_width, point_start, point_end): + point_center = self._point_center + winding_ccw = self._winding_ccw + + vec_start = point_start - point_center + vec_end = point_end - point_center + + radius = np.linalg.norm(vec_start) + + unit_vec_start = vec_start / radius + unit_vec_end = vec_end / radius + + # angle_start = np.arccos(unit_vec_start[0]) + # if unit_vec_start[1] < 0: + # angle_start = 2 * np.pi - angle_start + + angle_arc = np.arccos(np.dot(unit_vec_start, unit_vec_end)) + determinant = np.linalg.det([vec_start, vec_end]) + + if (winding_ccw and determinant < 0) or ( + not winding_ccw and determinant > 0): + angle_arc = 2 * np.pi - angle_arc + + arc_length = angle_arc * radius + + num_raster_segments = int(np.round(arc_length / raster_width)) + delta_angle = angle_arc / num_raster_segments + rotation_angles = np.arange(0, angle_arc, delta_angle) + + point_start_3d = np.append(vec_start, [0]) + raster_data = point_start_3d * np.ones((num_raster_segments, 3)) + + raster_data = raster_data[:, :, np.newaxis] + + r = R.from_euler('z', rotation_angles).as_dcm() + + raster_data = np.matmul(r, raster_data) + + raster_data = raster_data + np.append(point_center, [0])[:, + np.newaxis] + + return raster_data[:, :, 0] + # Private methods --------------------------------------------------------- def __init__(self, point0, point1, segment=LineSegment()): @@ -200,3 +261,18 @@ def num_points(self): :return: number of points """ return self._points[:, 0].size + + def rasterize(self, raster_width): + points = self._points + segments = self._segments + + raster_data = np.empty([0, 3]) + for i in range(self.num_segments()): + raster_data = np.vstack((raster_data, + segments[i].rasterize(raster_width, + points[i], + points[i + 1]))) + + raster_data = np.vstack( + (raster_data, np.append(self._points[-1], [0]))) + return raster_data From 80f36450d1927510a166c4bd3a6645132b107b23 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Thu, 2 Jan 2020 18:09:52 +0100 Subject: [PATCH 008/177] Refine arc segment rasterization --- mypackage/geometry.py | 88 ++++++++++++++++++++++++++----------------- 1 file changed, 53 insertions(+), 35 deletions(-) diff --git a/mypackage/geometry.py b/mypackage/geometry.py index e878bbe..6e89eb1 100644 --- a/mypackage/geometry.py +++ b/mypackage/geometry.py @@ -87,6 +87,56 @@ def __init__(self, center, winding_ccw=True): self._point_center = point_center self._winding_ccw = winding_ccw + def _arc_angle_and_length(self, vec_start, vec_end): + winding_ccw = self._winding_ccw + radius = np.linalg.norm(vec_start) + + unit_vec_start = vec_start / radius + unit_vec_end = vec_end / radius + + arc_angle = np.arccos(np.dot(unit_vec_start, unit_vec_end)) + determinant = np.linalg.det([vec_start, vec_end]) + + if (winding_ccw and determinant < 0) or ( + not winding_ccw and determinant > 0): + arc_angle = 2 * np.pi - arc_angle + + arc_length = arc_angle * radius + + return [arc_angle, arc_length] + + def _rotation_angles(self, vec_start, vec_end, raster_width): + winding_ccw = self._winding_ccw + [angle_arc, arc_length] = self._arc_angle_and_length(vec_start, + vec_end) + + num_raster_segments = int(np.round(arc_length / raster_width)) + delta_angle = angle_arc / num_raster_segments + + sign_multiplier = 1 + if not winding_ccw: + sign_multiplier = -1 + + rotation_angles = np.arange(0, sign_multiplier * angle_arc, + sign_multiplier * delta_angle) + + return rotation_angles + + def _rasterize(self, vec_start, rotation_angles): + vec_start_3d = np.append(vec_start, [0]) + raster_data = vec_start_3d * np.ones((len(rotation_angles), 3)) + + raster_data = raster_data[:, :, np.newaxis] + + rotation_matrices = R.from_euler('z', rotation_angles).as_dcm() + + raster_data = np.matmul(rotation_matrices, raster_data) + + raster_data = raster_data + np.append(self._point_center, [0])[:, + np.newaxis] + + return raster_data[:, :, 0] + def check_valid(self, point_start, point_end): """ Check if the segments data is valid. @@ -111,46 +161,14 @@ def check_valid(self, point_start, point_end): def rasterize(self, raster_width, point_start, point_end): point_center = self._point_center - winding_ccw = self._winding_ccw vec_start = point_start - point_center vec_end = point_end - point_center - radius = np.linalg.norm(vec_start) - - unit_vec_start = vec_start / radius - unit_vec_end = vec_end / radius - - # angle_start = np.arccos(unit_vec_start[0]) - # if unit_vec_start[1] < 0: - # angle_start = 2 * np.pi - angle_start - - angle_arc = np.arccos(np.dot(unit_vec_start, unit_vec_end)) - determinant = np.linalg.det([vec_start, vec_end]) - - if (winding_ccw and determinant < 0) or ( - not winding_ccw and determinant > 0): - angle_arc = 2 * np.pi - angle_arc - - arc_length = angle_arc * radius + rotation_angles = self._rotation_angles(vec_start, vec_end, + raster_width) - num_raster_segments = int(np.round(arc_length / raster_width)) - delta_angle = angle_arc / num_raster_segments - rotation_angles = np.arange(0, angle_arc, delta_angle) - - point_start_3d = np.append(vec_start, [0]) - raster_data = point_start_3d * np.ones((num_raster_segments, 3)) - - raster_data = raster_data[:, :, np.newaxis] - - r = R.from_euler('z', rotation_angles).as_dcm() - - raster_data = np.matmul(r, raster_data) - - raster_data = raster_data + np.append(point_center, [0])[:, - np.newaxis] - - return raster_data[:, :, 0] + return self._rasterize(vec_start, rotation_angles) # Private methods --------------------------------------------------------- From b3bab5c65534fd5911c035485740a99140653be7 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Thu, 2 Jan 2020 18:18:15 +0100 Subject: [PATCH 009/177] Optimize arc segment rasterization --- mypackage/geometry.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/mypackage/geometry.py b/mypackage/geometry.py index 6e89eb1..d78f3dc 100644 --- a/mypackage/geometry.py +++ b/mypackage/geometry.py @@ -123,18 +123,15 @@ def _rotation_angles(self, vec_start, vec_end, raster_width): return rotation_angles def _rasterize(self, vec_start, rotation_angles): - vec_start_3d = np.append(vec_start, [0]) - raster_data = vec_start_3d * np.ones((len(rotation_angles), 3)) - raster_data = raster_data[:, :, np.newaxis] + vec_start_3d = np.append(vec_start, [0])[np.newaxis, :, np.newaxis] + point_center_3d = np.append(self._point_center, [0])[:, np.newaxis] rotation_matrices = R.from_euler('z', rotation_angles).as_dcm() - raster_data = np.matmul(rotation_matrices, raster_data) - - raster_data = raster_data + np.append(self._point_center, [0])[:, - np.newaxis] - + raster_data = np.matmul(rotation_matrices, + vec_start_3d) + point_center_3d + return raster_data[:, :, 0] def check_valid(self, point_start, point_end): From 4617b9b2429f339c870c54380b798581c9672a58 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Fri, 3 Jan 2020 09:49:02 +0100 Subject: [PATCH 010/177] Add docstrings to new methods --- mypackage/geometry.py | 71 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 70 insertions(+), 1 deletion(-) diff --git a/mypackage/geometry.py b/mypackage/geometry.py index d78f3dc..e8e48d6 100644 --- a/mypackage/geometry.py +++ b/mypackage/geometry.py @@ -54,12 +54,36 @@ def check_valid(self, point_start, point_end): """ def rasterize(self, raster_width, point_start, point_end): + """ + Create an array of points that describe the segments contour. + + The effective raster width may vary from the specified one, + since the algorithm enforces constant distances between two + raster points. + + :param raster_width: The desired distance between two raster points + :param point_start: Starting point of the segment + :param point_end: End point of the segment + :return: Array of contour points (3d) + """ raise Exception("Function not defined for this segment type.") class LineSegment(Segment): """Line segment.""" def rasterize(self, raster_width, point_start, point_end): + """ + Create an array of points that describe the segments contour. + + The effective raster width may vary from the specified one, + since the algorithm enforces constant distances between two + raster points. + + :param raster_width: The desired distance between two raster points + :param point_start: Starting point of the segment + :param point_end: End point of the segment + :return: Array of contour points (3d) + """ length = np.linalg.norm(point_end - point_start) num_raster_segments = np.round(length / raster_width) nrw = 1. / num_raster_segments @@ -88,6 +112,13 @@ def __init__(self, center, winding_ccw=True): self._winding_ccw = winding_ccw def _arc_angle_and_length(self, vec_start, vec_end): + """ + Calculate the arcs angle and the arc length. + + :param vec_start: Vector from the arcs center to the starting point + :param vec_end: Vector from the arcs center to the end point + :return: Array containing the arcs angle and arc length + """ winding_ccw = self._winding_ccw radius = np.linalg.norm(vec_start) @@ -106,6 +137,16 @@ def _arc_angle_and_length(self, vec_start, vec_end): return [arc_angle, arc_length] def _rotation_angles(self, vec_start, vec_end, raster_width): + """ + Calculate the rotation angle of each raster point. + + The angles are referring to the vector to the starting point. + + :param vec_start: Vector from the arcs center to the starting point + :param vec_end: Vector from the arcs center to the end point + :param raster_width: Desired raster width + :return: Array containing the rotation angles + """ winding_ccw = self._winding_ccw [angle_arc, arc_length] = self._arc_angle_and_length(vec_start, vec_end) @@ -123,7 +164,13 @@ def _rotation_angles(self, vec_start, vec_end, raster_width): return rotation_angles def _rasterize(self, vec_start, rotation_angles): + """ + Create an array of points that describe the segments contour. + :param vec_start: Vector from the arcs center to the starting point + :param rotation_angles: Array containing the rotation angles + :return: Array of contour points (3d) + """ vec_start_3d = np.append(vec_start, [0])[np.newaxis, :, np.newaxis] point_center_3d = np.append(self._point_center, [0])[:, np.newaxis] @@ -131,7 +178,7 @@ def _rasterize(self, vec_start, rotation_angles): raster_data = np.matmul(rotation_matrices, vec_start_3d) + point_center_3d - + return raster_data[:, :, 0] def check_valid(self, point_start, point_end): @@ -157,6 +204,18 @@ def check_valid(self, point_start, point_end): "given center of the arc.") def rasterize(self, raster_width, point_start, point_end): + """ + Create an array of points that describe the segments contour. + + The effective raster width may vary from the specified one, + since the algorithm enforces constant distances between two + raster points. + + :param raster_width: The desired distance between two raster points + :param point_start: Starting point of the segment + :param point_end: End point of the segment + :return: Array of contour points (3d) + """ point_center = self._point_center vec_start = point_start - point_center @@ -278,6 +337,16 @@ def num_points(self): return self._points[:, 0].size def rasterize(self, raster_width): + """ + Create an array of points that describe the shapes contour. + + The effective raster width may vary from the specified one, + since the algorithm enforces constant distances between two + raster points inside of each segment. + + :param raster_width: The desired distance between two raster points + :return: Array of contour points (3d) + """ points = self._points segments = self._segments From 67a1f724b4b05384cb93845b48efe42cc7360a82 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Fri, 3 Jan 2020 12:01:49 +0100 Subject: [PATCH 011/177] Add test for arc segments --- mypackage/geometry.py | 3 +- tests/test_geometry.py | 81 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 1 deletion(-) diff --git a/mypackage/geometry.py b/mypackage/geometry.py index e8e48d6..1a7199e 100644 --- a/mypackage/geometry.py +++ b/mypackage/geometry.py @@ -158,7 +158,8 @@ def _rotation_angles(self, vec_start, vec_end, raster_width): if not winding_ccw: sign_multiplier = -1 - rotation_angles = np.arange(0, sign_multiplier * angle_arc, + rotation_angles = np.arange(0, sign_multiplier * ( + angle_arc - 0.5 * delta_angle), sign_multiplier * delta_angle) return rotation_angles diff --git a/tests/test_geometry.py b/tests/test_geometry.py index d85f7bb..083b1f5 100644 --- a/tests/test_geometry.py +++ b/tests/test_geometry.py @@ -1,5 +1,6 @@ import pytest import mypackage.geometry as geo +import numpy as np def test_shape2d_construction(): @@ -63,3 +64,83 @@ def test_shape2d_with_arc_segment(): # Invalid center point with pytest.raises(ValueError): shape.add_segment([3, 1], segment=geo.Shape2D.ArcSegment([2.1, 1])) + + +def arc_segment_test(point_center, point_start, point_end, raster_width, + winding_ccw, check_winding): + point_center = np.array(point_center) + point_start = np.array(point_start) + point_end = np.array(point_end) + + radius_arc = np.linalg.norm(point_start - point_center) + + arc_segment = geo.Shape2D.ArcSegment(point_center, winding_ccw=winding_ccw) + arc_segment.check_valid(point_start, point_end) + + data_ccw = arc_segment.rasterize(raster_width, point_start, point_end) + + # Check if first and last point of the data are identical to the + # specified segment start and end + data_ccw_start = data_ccw[0] + assert np.linalg.norm(data_ccw_start[0:2] - point_start) < 1E-9 + + num_data_points = data_ccw[:, 0].size + for i in range(num_data_points): + point = data_ccw[i] + + # Check that z-component is 0 + assert point[2] == 0. + + point = point[0:2] + + # Check if points are not rasterized clockwise + assert (check_winding(point_start, point_center)) + + # Check that points have the correct distance to the arcs center + distance_center_point = np.linalg.norm(point - point_center) + assert np.abs(distance_center_point - radius_arc) < 1E-6 + + # Check if the raster width is close to the specified value + if i < num_data_points - 1: + next_point = data_ccw[i + 1, 0:2] + else: + next_point = point_end + + raster_width_eff = np.linalg.norm(next_point - point) + if not np.abs(raster_width_eff - raster_width) < 0.1 * raster_width: + print(point) + print(next_point) + print(raster_width_eff) + assert np.abs(raster_width_eff - raster_width) < 0.1 * raster_width + + +def test_arc_segment_rasterizaion(): + # center right of segment line + # ---------------------------- + + point_center = [3, 2] + point_start = [1, 2] + point_end = [3, 4] + raster_width = 0.2 + + def in_second_quadrant(p, c): return (p[0] <= c[0] and p[1] >= c[1]) + + def not_in_second_quadrant(p, c): return not (p[0] < c[0] and p[1] > c[1]) + + arc_segment_test(point_center, point_start, point_end, raster_width, False, + in_second_quadrant) + arc_segment_test(point_center, point_start, point_end, raster_width, True, + not_in_second_quadrant) + + # center left of segment line + # ---------------------------- + + point_center = [-4, -7] + point_start = [-4, -2] + point_end = [-9, -7] + raster_width = 0.1 + + arc_segment_test(point_center, point_start, point_end, raster_width, False, + not_in_second_quadrant) + arc_segment_test(point_center, point_start, point_end, raster_width, True, + in_second_quadrant) From 9a22234519b7fd77b45e3cea778c9312bd15db63 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Fri, 3 Jan 2020 12:06:46 +0100 Subject: [PATCH 012/177] Fix line segment rasterization --- mypackage/geometry.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/mypackage/geometry.py b/mypackage/geometry.py index 1a7199e..3870996 100644 --- a/mypackage/geometry.py +++ b/mypackage/geometry.py @@ -85,6 +85,10 @@ def rasterize(self, raster_width, point_start, point_end): :return: Array of contour points (3d) """ length = np.linalg.norm(point_end - point_start) + + point_start = np.append(point_start, [0]) + point_end = np.append(point_end, [0]) + num_raster_segments = np.round(length / raster_width) nrw = 1. / num_raster_segments @@ -159,7 +163,7 @@ def _rotation_angles(self, vec_start, vec_end, raster_width): sign_multiplier = -1 rotation_angles = np.arange(0, sign_multiplier * ( - angle_arc - 0.5 * delta_angle), + angle_arc - 0.5 * delta_angle), sign_multiplier * delta_angle) return rotation_angles From 1d48039c43957670636e894b907c15b8dcabbaec Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Fri, 3 Jan 2020 12:46:07 +0100 Subject: [PATCH 013/177] Add line segment rasterization test --- tests/test_geometry.py | 60 +++++++++++++++++++++++++++++++++--------- 1 file changed, 48 insertions(+), 12 deletions(-) diff --git a/tests/test_geometry.py b/tests/test_geometry.py index 083b1f5..bb7326b 100644 --- a/tests/test_geometry.py +++ b/tests/test_geometry.py @@ -66,6 +66,48 @@ def test_shape2d_with_arc_segment(): shape.add_segment([3, 1], segment=geo.Shape2D.ArcSegment([2.1, 1])) +def test_line_segment_rasterizaion(): + point_start = np.array([3, -5]) + point_end = np.array([-4, 1]) + raster_width = 0.2 + + line_segment = geo.Shape2D.LineSegment() + + data = line_segment.rasterize(raster_width, point_start, point_end) + + # Check if first point of the data are identical to the segment start + assert np.linalg.norm(data[0, 0:2] - point_start) < 1E-9 + + vec_start_end = point_end - point_start + + num_data_points = data[:, 0].size + for i in range(num_data_points): + point = data[i] + + # Check that z-component is 0 + assert point[2] == 0. + + point = point[0:2] + + # Check if point is on line + vec_start_point = point - point_start + assert np.abs(np.linalg.det([vec_start_end, vec_start_point])) < 1E-6 + + # Check if point lies between start and end + dot_product = np.dot(vec_start_point, vec_start_end) + assert dot_product >= 0 + assert dot_product < np.dot(vec_start_end, vec_start_end) + + # Check if the raster width is close to the specified value + if i < num_data_points - 1: + next_point = data[i + 1, 0:2] + else: + next_point = point_end + + raster_width_eff = np.linalg.norm(next_point - point) + assert np.abs(raster_width_eff - raster_width) < 0.1 * raster_width + + def arc_segment_test(point_center, point_start, point_end, raster_width, winding_ccw, check_winding): point_center = np.array(point_center) @@ -77,16 +119,14 @@ def arc_segment_test(point_center, point_start, point_end, raster_width, arc_segment = geo.Shape2D.ArcSegment(point_center, winding_ccw=winding_ccw) arc_segment.check_valid(point_start, point_end) - data_ccw = arc_segment.rasterize(raster_width, point_start, point_end) + data = arc_segment.rasterize(raster_width, point_start, point_end) - # Check if first and last point of the data are identical to the - # specified segment start and end - data_ccw_start = data_ccw[0] - assert np.linalg.norm(data_ccw_start[0:2] - point_start) < 1E-9 + # Check if first point of the data are identical to the segment start + assert np.linalg.norm(data[0, 0:2] - point_start) < 1E-9 - num_data_points = data_ccw[:, 0].size + num_data_points = data[:, 0].size for i in range(num_data_points): - point = data_ccw[i] + point = data[i] # Check that z-component is 0 assert point[2] == 0. @@ -102,15 +142,11 @@ def arc_segment_test(point_center, point_start, point_end, raster_width, # Check if the raster width is close to the specified value if i < num_data_points - 1: - next_point = data_ccw[i + 1, 0:2] + next_point = data[i + 1, 0:2] else: next_point = point_end raster_width_eff = np.linalg.norm(next_point - point) - if not np.abs(raster_width_eff - raster_width) < 0.1 * raster_width: - print(point) - print(next_point) - print(raster_width_eff) assert np.abs(raster_width_eff - raster_width) < 0.1 * raster_width From 5cd7b790544aa89776f5c0a5bc82ff582754a2ee Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Fri, 3 Jan 2020 13:27:08 +0100 Subject: [PATCH 014/177] Fix flake8 errors --- mypackage/geometry.py | 9 +++++---- tests/test_geometry.py | 8 +++++--- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/mypackage/geometry.py b/mypackage/geometry.py index 3870996..9dde458 100644 --- a/mypackage/geometry.py +++ b/mypackage/geometry.py @@ -85,7 +85,7 @@ def rasterize(self, raster_width, point_start, point_end): :return: Array of contour points (3d) """ length = np.linalg.norm(point_end - point_start) - + point_start = np.append(point_start, [0]) point_end = np.append(point_end, [0]) @@ -162,9 +162,10 @@ def _rotation_angles(self, vec_start, vec_end, raster_width): if not winding_ccw: sign_multiplier = -1 - rotation_angles = np.arange(0, sign_multiplier * ( - angle_arc - 0.5 * delta_angle), - sign_multiplier * delta_angle) + rotation_angles = np.arange( + 0, + sign_multiplier * (angle_arc - 0.5 * delta_angle), + sign_multiplier * delta_angle) return rotation_angles diff --git a/tests/test_geometry.py b/tests/test_geometry.py index bb7326b..1284f03 100644 --- a/tests/test_geometry.py +++ b/tests/test_geometry.py @@ -97,7 +97,7 @@ def test_line_segment_rasterizaion(): dot_product = np.dot(vec_start_point, vec_start_end) assert dot_product >= 0 assert dot_product < np.dot(vec_start_end, vec_start_end) - + # Check if the raster width is close to the specified value if i < num_data_points - 1: next_point = data[i + 1, 0:2] @@ -159,9 +159,11 @@ def test_arc_segment_rasterizaion(): point_end = [3, 4] raster_width = 0.2 - def in_second_quadrant(p, c): return (p[0] <= c[0] and p[1] >= c[1]) + def in_second_quadrant(p, c): + return p[0] <= c[0] and p[1] >= c[1] - def not_in_second_quadrant(p, c): return not (p[0] < c[0] and p[1] > c[1]) + def not_in_second_quadrant(p, c): + return not (p[0] < c[0] and p[1] > c[1]) arc_segment_test(point_center, point_start, point_end, raster_width, False, in_second_quadrant) From f77eb563ed6fa31d64bd919a62dcdeae4ea552bf Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Fri, 3 Jan 2020 13:38:10 +0100 Subject: [PATCH 015/177] Remove unnecessary function from segment base class --- mypackage/geometry.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/mypackage/geometry.py b/mypackage/geometry.py index 9dde458..4958f06 100644 --- a/mypackage/geometry.py +++ b/mypackage/geometry.py @@ -53,21 +53,6 @@ def check_valid(self, point_start, point_end): :return: --- """ - def rasterize(self, raster_width, point_start, point_end): - """ - Create an array of points that describe the segments contour. - - The effective raster width may vary from the specified one, - since the algorithm enforces constant distances between two - raster points. - - :param raster_width: The desired distance between two raster points - :param point_start: Starting point of the segment - :param point_end: End point of the segment - :return: Array of contour points (3d) - """ - raise Exception("Function not defined for this segment type.") - class LineSegment(Segment): """Line segment.""" From 6c7dd3bbb0d492ad374ffc146f2093a4a5b6833a Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Fri, 3 Jan 2020 14:18:09 +0100 Subject: [PATCH 016/177] Remove code dulications --- tests/test_geometry.py | 65 ++++++++++++++++++------------------------ 1 file changed, 28 insertions(+), 37 deletions(-) diff --git a/tests/test_geometry.py b/tests/test_geometry.py index 1284f03..b2f489c 100644 --- a/tests/test_geometry.py +++ b/tests/test_geometry.py @@ -66,31 +66,45 @@ def test_shape2d_with_arc_segment(): shape.add_segment([3, 1], segment=geo.Shape2D.ArcSegment([2.1, 1])) +def default_rasterization_tests(data, raster_width, point_start, point_end): + # Check if first point of the data are identical to the segment start + assert np.linalg.norm(data[0, 0:2] - point_start) < 1E-9 + + num_data_points = data[:, 0].size + for i in range(num_data_points): + point = data[i] + + # Check that z-component is 0 + assert point[2] == 0. + + # Check if the raster width is close to the specified value + if i < num_data_points - 1: + next_point = data[i + 1, 0:2] + else: + next_point = point_end + + raster_width_eff = np.linalg.norm(next_point - point[0:2]) + assert np.abs(raster_width_eff - raster_width) < 0.1 * raster_width + + def test_line_segment_rasterizaion(): point_start = np.array([3, -5]) point_end = np.array([-4, 1]) raster_width = 0.2 + vec_start_end = point_end - point_start line_segment = geo.Shape2D.LineSegment() - data = line_segment.rasterize(raster_width, point_start, point_end) - # Check if first point of the data are identical to the segment start - assert np.linalg.norm(data[0, 0:2] - point_start) < 1E-9 - - vec_start_end = point_end - point_start + # Perform standard segment rasterization tests + default_rasterization_tests(data, raster_width, point_start, point_end) num_data_points = data[:, 0].size for i in range(num_data_points): point = data[i] - # Check that z-component is 0 - assert point[2] == 0. - - point = point[0:2] - # Check if point is on line - vec_start_point = point - point_start + vec_start_point = point[0:2] - point_start assert np.abs(np.linalg.det([vec_start_end, vec_start_point])) < 1E-6 # Check if point lies between start and end @@ -98,15 +112,6 @@ def test_line_segment_rasterizaion(): assert dot_product >= 0 assert dot_product < np.dot(vec_start_end, vec_start_end) - # Check if the raster width is close to the specified value - if i < num_data_points - 1: - next_point = data[i + 1, 0:2] - else: - next_point = point_end - - raster_width_eff = np.linalg.norm(next_point - point) - assert np.abs(raster_width_eff - raster_width) < 0.1 * raster_width - def arc_segment_test(point_center, point_start, point_end, raster_width, winding_ccw, check_winding): @@ -121,34 +126,20 @@ def arc_segment_test(point_center, point_start, point_end, raster_width, data = arc_segment.rasterize(raster_width, point_start, point_end) - # Check if first point of the data are identical to the segment start - assert np.linalg.norm(data[0, 0:2] - point_start) < 1E-9 + # Perform standard segment rasterization tests + default_rasterization_tests(data, raster_width, point_start, point_end) num_data_points = data[:, 0].size for i in range(num_data_points): point = data[i] - # Check that z-component is 0 - assert point[2] == 0. - - point = point[0:2] - # Check if points are not rasterized clockwise assert (check_winding(point_start, point_center)) # Check that points have the correct distance to the arcs center - distance_center_point = np.linalg.norm(point - point_center) + distance_center_point = np.linalg.norm(point[0:2] - point_center) assert np.abs(distance_center_point - radius_arc) < 1E-6 - # Check if the raster width is close to the specified value - if i < num_data_points - 1: - next_point = data[i + 1, 0:2] - else: - next_point = point_end - - raster_width_eff = np.linalg.norm(next_point - point) - assert np.abs(raster_width_eff - raster_width) < 0.1 * raster_width - def test_arc_segment_rasterizaion(): # center right of segment line From d0254ec15f2a0947177d2d4259a35b34298ca5e5 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Fri, 3 Jan 2020 14:26:56 +0100 Subject: [PATCH 017/177] Make rasterize method of line segment static --- mypackage/geometry.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mypackage/geometry.py b/mypackage/geometry.py index 4958f06..e6a0555 100644 --- a/mypackage/geometry.py +++ b/mypackage/geometry.py @@ -56,7 +56,8 @@ def check_valid(self, point_start, point_end): class LineSegment(Segment): """Line segment.""" - def rasterize(self, raster_width, point_start, point_end): + @staticmethod + def rasterize(raster_width, point_start, point_end): """ Create an array of points that describe the segments contour. From 651dd72dd79a0905f660c1dd1c8078958b07fecd Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Fri, 3 Jan 2020 14:33:39 +0100 Subject: [PATCH 018/177] Add some ignored words to typo-ci --- .typo-ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.typo-ci.yml b/.typo-ci.yml index b7dc05c..ae9f7ef 100644 --- a/.typo-ci.yml +++ b/.typo-ci.yml @@ -27,6 +27,9 @@ excluded_files: excluded_words: - typoci - numpy + - scipy + - matmul + - arange - vstack - pytest - linalg From 37b22bf230d2ca8408c6c115e668a05da26245e9 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Fri, 3 Jan 2020 15:43:57 +0100 Subject: [PATCH 019/177] Replace branch statement --- mypackage/geometry.py | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/mypackage/geometry.py b/mypackage/geometry.py index e6a0555..6ce671c 100644 --- a/mypackage/geometry.py +++ b/mypackage/geometry.py @@ -99,7 +99,10 @@ def __init__(self, center, winding_ccw=True): check_point_data_valid(point_center) self._point_center = point_center - self._winding_ccw = winding_ccw + if winding_ccw: + self._sign_winding = 1 + else: + self._sign_winding = -1 def _arc_angle_and_length(self, vec_start, vec_end): """ @@ -109,18 +112,21 @@ def _arc_angle_and_length(self, vec_start, vec_end): :param vec_end: Vector from the arcs center to the end point :return: Array containing the arcs angle and arc length """ - winding_ccw = self._winding_ccw + sign_winding = self._sign_winding radius = np.linalg.norm(vec_start) + # Calculate angle between vectors (always the smaller one) unit_vec_start = vec_start / radius unit_vec_end = vec_end / radius + angle_vecs = np.arccos(np.dot(unit_vec_start, unit_vec_end)) - arc_angle = np.arccos(np.dot(unit_vec_start, unit_vec_end)) + # If the determinant is < 0 point_end is on the right of + # vec_start. Otherwise it is on the left-hand side. determinant = np.linalg.det([vec_start, vec_end]) + sign_combined = np.sign(sign_winding * determinant) - if (winding_ccw and determinant < 0) or ( - not winding_ccw and determinant > 0): - arc_angle = 2 * np.pi - arc_angle + arc_angle = 2 * np.pi * np.ceil( + (1 - sign_combined) / 2) + sign_combined * angle_vecs arc_length = arc_angle * radius @@ -137,21 +143,16 @@ def _rotation_angles(self, vec_start, vec_end, raster_width): :param raster_width: Desired raster width :return: Array containing the rotation angles """ - winding_ccw = self._winding_ccw + sign = self._sign_winding [angle_arc, arc_length] = self._arc_angle_and_length(vec_start, vec_end) num_raster_segments = int(np.round(arc_length / raster_width)) delta_angle = angle_arc / num_raster_segments - sign_multiplier = 1 - if not winding_ccw: - sign_multiplier = -1 - - rotation_angles = np.arange( - 0, - sign_multiplier * (angle_arc - 0.5 * delta_angle), - sign_multiplier * delta_angle) + rotation_angles = np.arange(0, + sign * (angle_arc - 0.5 * delta_angle), + sign * delta_angle) return rotation_angles From 074bc806e286157e09a368b882da9f36ecff04e0 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Fri, 3 Jan 2020 16:16:10 +0100 Subject: [PATCH 020/177] Add test for shape rasterization --- tests/test_geometry.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/test_geometry.py b/tests/test_geometry.py index b2f489c..6a9fb20 100644 --- a/tests/test_geometry.py +++ b/tests/test_geometry.py @@ -173,3 +173,26 @@ def not_in_second_quadrant(p, c): not_in_second_quadrant) arc_segment_test(point_center, point_start, point_end, raster_width, True, in_second_quadrant) + + +def test_shape2d_rasterization(): + points = np.array([[0, 0], + [0, 1], + [1, 1], + [1, 0]]) + raster_width = 0.2 + + shape = geo.Shape2D(points[0], points[1]) + shape.add_segment(points[2]) + shape.add_segment(points[3]) + + data = shape.rasterize(raster_width) + + # Segment points must be included + for point in points: + assert geo.is_row_in_array(point, data[:, 0:2]) + + # check effective raster width + for i in range(1, data[:, 0].size): + raster_width_eff = np.linalg.norm(data[i] - data[i - 1]) + assert np.abs(raster_width_eff - raster_width) < 0.1 * raster_width From 5775b59fb9250ef83ee67d607e74e60732fa141f Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Fri, 3 Jan 2020 16:24:11 +0100 Subject: [PATCH 021/177] Remove my_funcs package --- mypackage/my_funcs.py | 29 ----------------------------- tests/test_my_funcs.py | 19 ------------------- 2 files changed, 48 deletions(-) delete mode 100644 mypackage/my_funcs.py delete mode 100644 tests/test_my_funcs.py diff --git a/mypackage/my_funcs.py b/mypackage/my_funcs.py deleted file mode 100644 index 0bdc908..0000000 --- a/mypackage/my_funcs.py +++ /dev/null @@ -1,29 +0,0 @@ -"""Contains some test functions.""" - - -def my_func(sel): - """ - Print a message. - - :param sel: Selects one of two possible messages - :return: --- - """ - if sel: - print("The answer is 42") - else: - print("This branch is not covered") - - -def add_numbers(in0, in1): - """ - Add 2 numbers. - - :param in0: First number - :param in1: Second number - :return: Result of the addition - """ - if not isinstance(in0, (int, float)): - raise TypeError("First argument must be a integer or float") - if not isinstance(in1, (int, float)): - raise TypeError("Second argument must be a integer or float") - return in0 + in1 diff --git a/tests/test_my_funcs.py b/tests/test_my_funcs.py deleted file mode 100644 index ed8265f..0000000 --- a/tests/test_my_funcs.py +++ /dev/null @@ -1,19 +0,0 @@ -import mypackage.my_funcs as mf -import pytest - - -def test_print_hello_world(): - print('Hello world') - - -def test_my_func(): - mf.my_func(True) - mf.my_func(False) - - -def test_add_numbers(): - assert mf.add_numbers(1, 2) == 3 - with pytest.raises(TypeError): - mf.add_numbers("1", 3) - with pytest.raises(TypeError): - mf.add_numbers(1, "3") From b224c158baf57d9d1490bd0d0cf938c244687715 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Fri, 3 Jan 2020 18:01:51 +0100 Subject: [PATCH 022/177] Add point cloud generator package --- mypackage/point_cloud_generator.py | 14 ++++++++++++++ tests/test_point_cloud_generator.py | 21 +++++++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 mypackage/point_cloud_generator.py create mode 100644 tests/test_point_cloud_generator.py diff --git a/mypackage/point_cloud_generator.py b/mypackage/point_cloud_generator.py new file mode 100644 index 0000000..30d846f --- /dev/null +++ b/mypackage/point_cloud_generator.py @@ -0,0 +1,14 @@ +import mypackage.geometry as geo + + +class Profile: + def __init__(self, shapes): + + if not isinstance(shapes, list): + shapes = [shapes] + + if not all(isinstance(shape, geo.Shape2D) for shape in shapes): + raise ValueError( + "Only instances or lists of Shape2d opbjects are accepted.") + + self.shapes = shapes diff --git a/tests/test_point_cloud_generator.py b/tests/test_point_cloud_generator.py new file mode 100644 index 0000000..3d516b0 --- /dev/null +++ b/tests/test_point_cloud_generator.py @@ -0,0 +1,21 @@ +import pytest +import mypackage.point_cloud_generator as pcg +import mypackage.geometry as geo + + +def test_profile_construction(): + shape = geo.Shape2D([0, 0], [1, 0]) + shape.add_segment([2, -1]) + shape.add_segment([0, -1]) + + # Check valid types + pcg.Profile(shape) + pcg.Profile([shape, shape]) + + # Check invalid types + with pytest.raises(ValueError): + pcg.Profile(3) + with pytest.raises(ValueError): + pcg.Profile("This is not right") + with pytest.raises(ValueError): + pcg.Profile([2, 8, 1]) From 213da0520133449eead108880e9bffa22a57458b Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Mon, 6 Jan 2020 09:54:29 +0100 Subject: [PATCH 023/177] Add docstrings --- mypackage/point_cloud_generator.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/mypackage/point_cloud_generator.py b/mypackage/point_cloud_generator.py index 30d846f..d766635 100644 --- a/mypackage/point_cloud_generator.py +++ b/mypackage/point_cloud_generator.py @@ -1,9 +1,17 @@ +"""Contains methods and classes to generate 3d point clouds.""" + import mypackage.geometry as geo class Profile: + """Defines a 2d profile.""" + def __init__(self, shapes): + """ + Check construction succeedConstruct profile class. + :param: shapes: Instance or list of geo.Shape2D class(es) + """ if not isinstance(shapes, list): shapes = [shapes] From c1d34009157ab1be6067d8bfa1a9c3267eff6681 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Mon, 6 Jan 2020 09:57:45 +0100 Subject: [PATCH 024/177] Correct a typo --- mypackage/point_cloud_generator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mypackage/point_cloud_generator.py b/mypackage/point_cloud_generator.py index d766635..57fdbbd 100644 --- a/mypackage/point_cloud_generator.py +++ b/mypackage/point_cloud_generator.py @@ -8,7 +8,7 @@ class Profile: def __init__(self, shapes): """ - Check construction succeedConstruct profile class. + Construct profile class. :param: shapes: Instance or list of geo.Shape2D class(es) """ @@ -17,6 +17,6 @@ def __init__(self, shapes): if not all(isinstance(shape, geo.Shape2D) for shape in shapes): raise ValueError( - "Only instances or lists of Shape2d opbjects are accepted.") + "Only instances or lists of Shape2d objects are accepted.") self.shapes = shapes From 04e95f4d18a0f54f31cab1ac2d098ee26822b061 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Mon, 6 Jan 2020 12:27:39 +0100 Subject: [PATCH 025/177] Add copy_and_reflect method to segments --- mypackage/geometry.py | 29 ++++++++++++++++++++++++++++- tests/test_geometry.py | 26 ++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/mypackage/geometry.py b/mypackage/geometry.py index 6ce671c..d1441b6 100644 --- a/mypackage/geometry.py +++ b/mypackage/geometry.py @@ -1,6 +1,7 @@ """Provides classes to define lines and surfaces.""" import numpy as np +import copy from scipy.spatial.transform import Rotation as R @@ -53,6 +54,15 @@ def check_valid(self, point_start, point_end): :return: --- """ + def copy_and_reflect(self, *kwargs): + """ + Create a reflected copy of the segment. + + :param kwargs: Key word arguments + :return: Reflected copy + """ + return copy.deepcopy(self) + class LineSegment(Segment): """Line segment.""" @@ -196,6 +206,22 @@ def check_valid(self, point_start, point_end): "Segment start and end points are not compatible with " "given center of the arc.") + def copy_and_reflect(self, reflection_matrix, offset=[0, 0]): + """ + Create a reflected copy of the arc segment. + + :param reflection_matrix: Reflection matrix + :param offset: Offset + :return: Reflected copy of the segment + """ + point_center_copy = np.matmul(reflection_matrix, + self._point_center - offset) + offset + if self._sign_winding < 0: + winding_ccw_new = True + else: + winding_ccw_new = False + return Shape2D.ArcSegment(point_center_copy, winding_ccw_new) + def rasterize(self, raster_width, point_start, point_end): """ Create an array of points that describe the segments contour. @@ -219,7 +245,8 @@ def rasterize(self, raster_width, point_start, point_end): return self._rasterize(vec_start, rotation_angles) - # Private methods --------------------------------------------------------- + # Private methods + # --------------------------------------------------------- def __init__(self, point0, point1, segment=LineSegment()): """ diff --git a/tests/test_geometry.py b/tests/test_geometry.py index 6a9fb20..a3a1561 100644 --- a/tests/test_geometry.py +++ b/tests/test_geometry.py @@ -196,3 +196,29 @@ def test_shape2d_rasterization(): for i in range(1, data[:, 0].size): raster_width_eff = np.linalg.norm(data[i] - data[i - 1]) assert np.abs(raster_width_eff - raster_width) < 0.1 * raster_width + + +def test_line_segment_copy_and_reflect(): + reflection_matrix = [[0, 1], [1, 0]] + + segment = geo.Shape2D.LineSegment() + segment_copy = segment.copy_and_reflect(reflection_matrix) + + # ensure that segment_copy is a deepcopy of segment + assert segment_copy is not segment + + +def test_arc_segment_copy_and_reflect(): + reflection_matrix = [[0, 1], [1, 0]] + offset = [2, -1] + point_center = [2, 3] + + segment = geo.Shape2D.ArcSegment(point_center) + segment_copy = segment.copy_and_reflect(reflection_matrix, offset) + + # ensure that segment_copy is a deepcopy of segment + assert segment_copy is not segment + + # Check if new center point is correct + assert segment_copy._point_center[0] == 6 + assert segment_copy._point_center[1] == -1 From 7c88649c430e4d72dff5876579acbdfe3ab0d0b7 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Mon, 6 Jan 2020 12:34:26 +0100 Subject: [PATCH 026/177] Fix deepsource issues --- mypackage/geometry.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/mypackage/geometry.py b/mypackage/geometry.py index d1441b6..1fc52eb 100644 --- a/mypackage/geometry.py +++ b/mypackage/geometry.py @@ -54,11 +54,12 @@ def check_valid(self, point_start, point_end): :return: --- """ - def copy_and_reflect(self, *kwargs): + def copy_and_reflect(self, reflection_matrix=None, offset=None): """ Create a reflected copy of the segment. - :param kwargs: Key word arguments + :param reflection_matrix: Reflection matrix + :param offset: Offset :return: Reflected copy """ return copy.deepcopy(self) @@ -206,7 +207,7 @@ def check_valid(self, point_start, point_end): "Segment start and end points are not compatible with " "given center of the arc.") - def copy_and_reflect(self, reflection_matrix, offset=[0, 0]): + def copy_and_reflect(self, reflection_matrix, offset=np.array([0, 0])): """ Create a reflected copy of the arc segment. From 8b137f23071168af64964693ac6871a12ecb8749 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Mon, 6 Jan 2020 12:37:25 +0100 Subject: [PATCH 027/177] Remove unnecessary branch --- mypackage/geometry.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/mypackage/geometry.py b/mypackage/geometry.py index 1fc52eb..c854ee8 100644 --- a/mypackage/geometry.py +++ b/mypackage/geometry.py @@ -217,10 +217,9 @@ def copy_and_reflect(self, reflection_matrix, offset=np.array([0, 0])): """ point_center_copy = np.matmul(reflection_matrix, self._point_center - offset) + offset - if self._sign_winding < 0: - winding_ccw_new = True - else: - winding_ccw_new = False + + winding_ccw_new = self._sign_winding < 0 + return Shape2D.ArcSegment(point_center_copy, winding_ccw_new) def rasterize(self, raster_width, point_start, point_end): From 631292d5bee966ad98bf2df17c64ac9854aeaa5b Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Mon, 6 Jan 2020 12:42:08 +0100 Subject: [PATCH 028/177] Fix new codacy issues --- mypackage/geometry.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mypackage/geometry.py b/mypackage/geometry.py index c854ee8..a0ff01a 100644 --- a/mypackage/geometry.py +++ b/mypackage/geometry.py @@ -54,7 +54,8 @@ def check_valid(self, point_start, point_end): :return: --- """ - def copy_and_reflect(self, reflection_matrix=None, offset=None): + def copy_and_reflect(self, _unused_reflection_matrix, + _unused_offset=np.array([0, 0])): """ Create a reflected copy of the segment. From ada25ff6db516e65d482ac24d1e676383c9c0e35 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Mon, 6 Jan 2020 15:13:16 +0100 Subject: [PATCH 029/177] Change copy_and_reflect to copy_and_transform --- mypackage/geometry.py | 44 ++++++++++++++++++++++++++++-------------- tests/test_geometry.py | 19 ++++++++++++++---- 2 files changed, 44 insertions(+), 19 deletions(-) diff --git a/mypackage/geometry.py b/mypackage/geometry.py index a0ff01a..fa0e8b2 100644 --- a/mypackage/geometry.py +++ b/mypackage/geometry.py @@ -54,14 +54,20 @@ def check_valid(self, point_start, point_end): :return: --- """ - def copy_and_reflect(self, _unused_reflection_matrix, - _unused_offset=np.array([0, 0])): + def copy_and_transform(self, + _unused_transformation_matrix=np.array( + [[1, 0], [0, 1]]), + _unused_translation_pre=np.array([0, 0]), + _unused_translation_post=np.array([0, 0])): """ - Create a reflected copy of the segment. - - :param reflection_matrix: Reflection matrix - :param offset: Offset - :return: Reflected copy + Create a transformed copy of the segment. + + :param _unused_transformation_matrix: Transformation matrix + :param _unused_translation_pre: Translation applied before the + matrix multiplication. + :param _unused_translation_post: Translation applied after the + matrix multiplication. + :return: Transformed copy """ return copy.deepcopy(self) @@ -208,16 +214,24 @@ def check_valid(self, point_start, point_end): "Segment start and end points are not compatible with " "given center of the arc.") - def copy_and_reflect(self, reflection_matrix, offset=np.array([0, 0])): + def copy_and_transform(self, + transformation_matrix=np.array( + [[1, 0], [0, 1]]), + translation_pre=np.array([0, 0]), + translation_post=np.array([0, 0])): """ - Create a reflected copy of the arc segment. - - :param reflection_matrix: Reflection matrix - :param offset: Offset - :return: Reflected copy of the segment + Create a transformed copy of the segment. + + :param transformation_matrix: Transformation matrix + :param translation_pre: Translation applied before the matrix + multiplication. + :param translation_post: Translation applied after the matrix + multiplication. + :return: Transformed copy """ - point_center_copy = np.matmul(reflection_matrix, - self._point_center - offset) + offset + point_center_copy = np.matmul(transformation_matrix, + self._point_center + + translation_pre) + translation_post winding_ccw_new = self._sign_winding < 0 diff --git a/tests/test_geometry.py b/tests/test_geometry.py index a3a1561..2173894 100644 --- a/tests/test_geometry.py +++ b/tests/test_geometry.py @@ -202,23 +202,34 @@ def test_line_segment_copy_and_reflect(): reflection_matrix = [[0, 1], [1, 0]] segment = geo.Shape2D.LineSegment() - segment_copy = segment.copy_and_reflect(reflection_matrix) + segment_copy = segment.copy_and_transform(reflection_matrix) # ensure that segment_copy is a deepcopy of segment assert segment_copy is not segment def test_arc_segment_copy_and_reflect(): - reflection_matrix = [[0, 1], [1, 0]] - offset = [2, -1] + reflection_matrix = np.array([[0, 1], [1, 0]]) + offset = np.array([2, -1]) point_center = [2, 3] segment = geo.Shape2D.ArcSegment(point_center) - segment_copy = segment.copy_and_reflect(reflection_matrix, offset) + segment_copy = segment.copy_and_transform(reflection_matrix, -offset, + offset) + segment_copy2 = segment_copy.copy_and_transform(reflection_matrix, -offset, + offset) # ensure that segment_copy is a deepcopy of segment assert segment_copy is not segment + assert segment_copy2 is not segment + assert segment_copy is not segment_copy2 # Check if new center point is correct assert segment_copy._point_center[0] == 6 assert segment_copy._point_center[1] == -1 + assert segment_copy2._point_center[0] == point_center[0] + assert segment_copy2._point_center[1] == point_center[1] + + # Check that winding order is changed + assert segment_copy._sign_winding == segment._sign_winding * -1 + assert segment_copy2._sign_winding == segment._sign_winding From a6c0c4fee5730768bc69707cce1e200e56458c5c Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Mon, 6 Jan 2020 15:17:48 +0100 Subject: [PATCH 030/177] Fix flake8 issue --- mypackage/geometry.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mypackage/geometry.py b/mypackage/geometry.py index fa0e8b2..0dbd0fa 100644 --- a/mypackage/geometry.py +++ b/mypackage/geometry.py @@ -230,8 +230,8 @@ def copy_and_transform(self, :return: Transformed copy """ point_center_copy = np.matmul(transformation_matrix, - self._point_center + - translation_pre) + translation_post + self._point_center + + translation_pre) + translation_post winding_ccw_new = self._sign_winding < 0 From 42bff93bcbc8b3bb8910f218c470fb2244b97450 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Mon, 6 Jan 2020 15:27:48 +0100 Subject: [PATCH 031/177] Configure flake8 to ignore W504 --- mypackage/geometry.py | 4 ++-- setup.cfg | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/mypackage/geometry.py b/mypackage/geometry.py index 0dbd0fa..fa0e8b2 100644 --- a/mypackage/geometry.py +++ b/mypackage/geometry.py @@ -230,8 +230,8 @@ def copy_and_transform(self, :return: Transformed copy """ point_center_copy = np.matmul(transformation_matrix, - self._point_center - + translation_pre) + translation_post + self._point_center + + translation_pre) + translation_post winding_ccw_new = self._sign_winding < 0 diff --git a/setup.cfg b/setup.cfg index 9555dce..215e82c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -4,6 +4,8 @@ # https://flake8.readthedocs.io/en/latest/user/error-codes.html # Note: there cannot be spaces after comma's here +ignore = + W504 # line break after binary operator max-line-length = 79 select = C,E,F,W,B,B950 # black formating options From 17922da57eb3436bbf0e805a639a17b5a0205cb1 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Tue, 7 Jan 2020 10:41:15 +0100 Subject: [PATCH 032/177] Fix a bug in the arc segments angle calculation --- mypackage/geometry.py | 81 +++++++++++++++++++++++++++++------------- tests/test_geometry.py | 37 ++++++++++++++++++- 2 files changed, 92 insertions(+), 26 deletions(-) diff --git a/mypackage/geometry.py b/mypackage/geometry.py index fa0e8b2..8d40f59 100644 --- a/mypackage/geometry.py +++ b/mypackage/geometry.py @@ -122,48 +122,55 @@ def __init__(self, center, winding_ccw=True): else: self._sign_winding = -1 - def _arc_angle_and_length(self, vec_start, vec_end): + def _arc_angle_and_length(self, vec_center_start, vec_center_end): """ Calculate the arcs angle and the arc length. - :param vec_start: Vector from the arcs center to the starting point - :param vec_end: Vector from the arcs center to the end point + :param vec_center_start: Vector from the arcs center to the + starting point + :param vec_center_end: Vector from the arcs center to the end point :return: Array containing the arcs angle and arc length """ sign_winding = self._sign_winding - radius = np.linalg.norm(vec_start) + radius = np.linalg.norm(vec_center_start) # Calculate angle between vectors (always the smaller one) - unit_vec_start = vec_start / radius - unit_vec_end = vec_end / radius - angle_vecs = np.arccos(np.dot(unit_vec_start, unit_vec_end)) + unit_center_start = vec_center_start / radius + unit_center_end = vec_center_end / np.linalg.norm(vec_center_end) + + angle_vecs = np.arccos( + np.dot(unit_center_start, unit_center_end)) # If the determinant is < 0 point_end is on the right of - # vec_start. Otherwise it is on the left-hand side. - determinant = np.linalg.det([vec_start, vec_end]) - sign_combined = np.sign(sign_winding * determinant) + # vec_center_start. Otherwise it is on the left-hand side. + determinant = np.linalg.det([vec_center_start, vec_center_end]) - arc_angle = 2 * np.pi * np.ceil( - (1 - sign_combined) / 2) + sign_combined * angle_vecs + if np.abs(np.sign(determinant) + sign_winding) > 0: + arc_angle = angle_vecs + else: + arc_angle = 2 * np.pi - angle_vecs arc_length = arc_angle * radius return [arc_angle, arc_length] - def _rotation_angles(self, vec_start, vec_end, raster_width): + def _rotation_angles(self, vec_center_start, vec_center_end, + raster_width): """ Calculate the rotation angle of each raster point. The angles are referring to the vector to the starting point. - :param vec_start: Vector from the arcs center to the starting point - :param vec_end: Vector from the arcs center to the end point + :param vec_center_start: Vector from the arcs center to the + starting point + :param vec_center_end: Vector from the arcs center to the end point :param raster_width: Desired raster width :return: Array containing the rotation angles """ sign = self._sign_winding - [angle_arc, arc_length] = self._arc_angle_and_length(vec_start, - vec_end) + [angle_arc, arc_length] = self._arc_angle_and_length( + vec_center_start, + vec_center_end) num_raster_segments = int(np.round(arc_length / raster_width)) delta_angle = angle_arc / num_raster_segments @@ -174,21 +181,23 @@ def _rotation_angles(self, vec_start, vec_end, raster_width): return rotation_angles - def _rasterize(self, vec_start, rotation_angles): + def _rasterize(self, vec_center_start, rotation_angles): """ Create an array of points that describe the segments contour. - :param vec_start: Vector from the arcs center to the starting point + :param vec_center_start: Vector from the arcs center to the + starting point :param rotation_angles: Array containing the rotation angles :return: Array of contour points (3d) """ - vec_start_3d = np.append(vec_start, [0])[np.newaxis, :, np.newaxis] + vec_center_start_3d = np.append(vec_center_start, + [0])[np.newaxis, :, np.newaxis] point_center_3d = np.append(self._point_center, [0])[:, np.newaxis] rotation_matrices = R.from_euler('z', rotation_angles).as_dcm() raster_data = np.matmul(rotation_matrices, - vec_start_3d) + point_center_3d + vec_center_start_3d) + point_center_3d return raster_data[:, :, 0] @@ -252,13 +261,14 @@ def rasterize(self, raster_width, point_start, point_end): """ point_center = self._point_center - vec_start = point_start - point_center - vec_end = point_end - point_center + vec_center_start = point_start - point_center + vec_center_end = point_end - point_center - rotation_angles = self._rotation_angles(vec_start, vec_end, + rotation_angles = self._rotation_angles(vec_center_start, + vec_center_end, raster_width) - return self._rasterize(vec_start, rotation_angles) + return self._rasterize(vec_center_start, rotation_angles) # Private methods # --------------------------------------------------------- @@ -338,6 +348,27 @@ def add_segment(self, point, segment=LineSegment()): self._points = np.vstack((self._points, point)) self._segments.append(segment) + def copy_and_reflect(self, normal_vec, offset=np.array([0, 0])): + + offset = np.array(offset) + + dot_product = np.dot(normal_vec, normal_vec) + outer_product = np.outer(normal_vec, normal_vec) + householder_matrix = np.identity(2) - 2 * outer_product / dot_product + + shape_copy = copy.deepcopy(self) + shape_copy._points -= offset + shape_copy._points = np.matmul(shape_copy._points, + np.transpose(householder_matrix)) + shape_copy._points += offset + + for i in range(len(shape_copy._segments)): + shape_copy._segments[i] = shape_copy._segments[ + i].copy_and_transform(householder_matrix, -offset, + offset) + + return shape_copy + def is_point_included(self, point): """ Check if a point is already part of the shape. diff --git a/tests/test_geometry.py b/tests/test_geometry.py index 2173894..c3cf79a 100644 --- a/tests/test_geometry.py +++ b/tests/test_geometry.py @@ -134,7 +134,7 @@ def arc_segment_test(point_center, point_start, point_end, raster_width, point = data[i] # Check if points are not rasterized clockwise - assert (check_winding(point_start, point_center)) + assert (check_winding(point[0:2], point_center)) # Check that points have the correct distance to the arcs center distance_center_point = np.linalg.norm(point[0:2] - point_center) @@ -174,6 +174,41 @@ def not_in_second_quadrant(p, c): arc_segment_test(point_center, point_start, point_end, raster_width, True, in_second_quadrant) + # center on segment line + # ---------------------- + + point_center = [3, 2] + point_start = [2, 2] + point_end = [4, 2] + raster_width = 0.1 + + def not_below_center(p, c): + return p[1] >= c[1] + + def not_above_center(p, c): + return p[1] <= c[1] + + arc_segment_test(point_center, point_start, point_end, raster_width, False, + not_below_center) + arc_segment_test(point_center, point_start, point_end, raster_width, True, + not_above_center) + + # special testcase + # ---------------- + # In a previous version the unit vectors to the start and end point were + # calculated using the norm of the vector to the start, since both + # vector length should be identical (radius). However, floating point + # errors caused the dot product to get greater than 1. In result, + # the angle between both vectors could not be calculated using the arccos. + # This test case will fail in this case. + point_center = [0, 0] + point_start = [-6.6 - 2.8] + point_end = [-6.4 - 4.2] + raster_width = 0.1 + + arc_segment = geo.Shape2D.ArcSegment(point_center) + arc_segment.rasterize(raster_width, point_start, point_end) + def test_shape2d_rasterization(): points = np.array([[0, 0], From d0671ee12dffd6e20067fdc30fd0759fd805fd48 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Tue, 7 Jan 2020 11:26:25 +0100 Subject: [PATCH 033/177] Clip input to arccos function to valid range --- mypackage/geometry.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/mypackage/geometry.py b/mypackage/geometry.py index 8d40f59..9bd0f1d 100644 --- a/mypackage/geometry.py +++ b/mypackage/geometry.py @@ -138,8 +138,8 @@ def _arc_angle_and_length(self, vec_center_start, vec_center_end): unit_center_start = vec_center_start / radius unit_center_end = vec_center_end / np.linalg.norm(vec_center_end) - angle_vecs = np.arccos( - np.dot(unit_center_start, unit_center_end)) + dot_unit = np.dot(unit_center_start, unit_center_end) + angle_vecs = np.arccos(np.clip(dot_unit, -1, 1)) # If the determinant is < 0 point_end is on the right of # vec_center_start. Otherwise it is on the left-hand side. @@ -286,7 +286,7 @@ def __init__(self, point0, point1, segment=LineSegment()): Shape2D._check_segment(segment, point0, point1) - self._points = np.array([point0, point1]) + self._points = np.array([point0, point1], dtype=float) self._segments = [segment] @staticmethod @@ -348,18 +348,21 @@ def add_segment(self, point, segment=LineSegment()): self._points = np.vstack((self._points, point)) self._segments.append(segment) - def copy_and_reflect(self, normal_vec, offset=np.array([0, 0])): + def copy_and_reflect(self, reflection_normal, distance_to_origin=0): + """Produces a copy of the shape and reflects it at a given axis""" - offset = np.array(offset) - - dot_product = np.dot(normal_vec, normal_vec) - outer_product = np.outer(normal_vec, normal_vec) + dot_product = np.dot(reflection_normal, reflection_normal) + outer_product = np.outer(reflection_normal, reflection_normal) householder_matrix = np.identity(2) - 2 * outer_product / dot_product + offset = np.array(reflection_normal) / np.sqrt( + dot_product) * distance_to_origin + shape_copy = copy.deepcopy(self) shape_copy._points -= offset shape_copy._points = np.matmul(shape_copy._points, np.transpose(householder_matrix)) + shape_copy._points += offset for i in range(len(shape_copy._segments)): From 460bc9d5dab9dbe8846f2075fc9b7623062e530f Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Tue, 7 Jan 2020 14:13:14 +0100 Subject: [PATCH 034/177] Finish Shape2d.copy_and_reflect method --- mypackage/geometry.py | 8 ++++++- tests/test_geometry.py | 51 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/mypackage/geometry.py b/mypackage/geometry.py index 9bd0f1d..c77481f 100644 --- a/mypackage/geometry.py +++ b/mypackage/geometry.py @@ -349,8 +349,14 @@ def add_segment(self, point, segment=LineSegment()): self._segments.append(segment) def copy_and_reflect(self, reflection_normal, distance_to_origin=0): - """Produces a copy of the shape and reflects it at a given axis""" + """ + Create a copy of the shape and reflect it at a given axis. + :param reflection_normal: Normal of the reflection axis + :param distance_to_origin: Distance of the reflection axis to the + origin + :return: Reflected copy of the shape + """ dot_product = np.dot(reflection_normal, reflection_normal) outer_product = np.outer(reflection_normal, reflection_normal) householder_matrix = np.identity(2) - 2 * outer_product / dot_product diff --git a/tests/test_geometry.py b/tests/test_geometry.py index c3cf79a..7150f61 100644 --- a/tests/test_geometry.py +++ b/tests/test_geometry.py @@ -243,7 +243,7 @@ def test_line_segment_copy_and_reflect(): assert segment_copy is not segment -def test_arc_segment_copy_and_reflect(): +def test_arc_segment_copy_and_transform(): reflection_matrix = np.array([[0, 1], [1, 0]]) offset = np.array([2, -1]) point_center = [2, 3] @@ -268,3 +268,52 @@ def test_arc_segment_copy_and_reflect(): # Check that winding order is changed assert segment_copy._sign_winding == segment._sign_winding * -1 assert segment_copy2._sign_winding == segment._sign_winding + + +def check_reflected_point(point, reflected_point, axis_offset, + direction_reflection_axis): + """Check if the midpoint lies on the reflection axis.""" + vec_original_reflected = reflected_point - point + mid_point = point + 0.5 * vec_original_reflected + shifted_mid_point = mid_point - axis_offset + determinant = np.linalg.det([shifted_mid_point, direction_reflection_axis]) + assert np.abs(determinant) < 1E-8 + + +def shape2d_copy_and_reflect_testcase(normal, distance_to_origin): + points = np.array([[3, 4], + [5, 0], + [11, 3]]) + point_center = np.array([6, 3]) + + direction_reflection_axis = np.array([normal[1], -normal[0]]) + normal_length = np.linalg.norm(normal) + unit_normal = np.array(normal) / normal_length + offset = distance_to_origin * unit_normal + + # create shape + arc_segment = geo.Shape2D.ArcSegment(point_center) + shape = geo.Shape2D(points[0], points[1], arc_segment) + shape.add_segment(points[2]) + + # create reflected shape + shape_reflected = shape.copy_and_reflect(normal, distance_to_origin) + + # check reflected points + check_reflected_point(point_center, + shape_reflected._segments[0]._point_center, offset, + direction_reflection_axis) + + for i in range(shape.num_points()): + check_reflected_point(shape._points[i], shape_reflected._points[i], + offset, direction_reflection_axis) + + +def test_shape2d_copy_and_reflect(): + shape2d_copy_and_reflect_testcase([2, 1], np.linalg.norm([2, 1])) + shape2d_copy_and_reflect_testcase([0, 1], 5) + shape2d_copy_and_reflect_testcase([1, 0], 3) + shape2d_copy_and_reflect_testcase([1, 0], -3) + shape2d_copy_and_reflect_testcase([-7, 2], 4.12) + shape2d_copy_and_reflect_testcase([-7, -2], 4.12) + shape2d_copy_and_reflect_testcase([7, -2], 4.12) From f12967f7c3c47898ed97f568610b70d75842c41d Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Tue, 7 Jan 2020 14:51:58 +0100 Subject: [PATCH 035/177] Fix deepsource issues --- mypackage/geometry.py | 36 ++++++++++++++++++++++++++---------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/mypackage/geometry.py b/mypackage/geometry.py index c77481f..86eaaab 100644 --- a/mypackage/geometry.py +++ b/mypackage/geometry.py @@ -348,6 +348,31 @@ def add_segment(self, point, segment=LineSegment()): self._points = np.vstack((self._points, point)) self._segments.append(segment) + def apply_transformation(self, + transformation_matrix=np.array([[1, 0], [0, 1]]), + translation_pre=np.array([0, 0]), + translation_post=np.array([0, 0])): + """ + Apply a transformation to the segment. + + :param transformation_matrix: Transformation matrix + :param translation_pre: Translation applied before the matrix + multiplication. + :param translation_post: Translation applied after the matrix + multiplication. + :return: --- + """ + self._points += translation_pre + self._points = np.matmul(self._points, + np.transpose(transformation_matrix)) + + self._points += translation_post + + for i in range(self.num_segments()): + self._segments[i] = self._segments[ + i].copy_and_transform(transformation_matrix, translation_pre, + translation_post) + def copy_and_reflect(self, reflection_normal, distance_to_origin=0): """ Create a copy of the shape and reflect it at a given axis. @@ -365,16 +390,7 @@ def copy_and_reflect(self, reflection_normal, distance_to_origin=0): dot_product) * distance_to_origin shape_copy = copy.deepcopy(self) - shape_copy._points -= offset - shape_copy._points = np.matmul(shape_copy._points, - np.transpose(householder_matrix)) - - shape_copy._points += offset - - for i in range(len(shape_copy._segments)): - shape_copy._segments[i] = shape_copy._segments[ - i].copy_and_transform(householder_matrix, -offset, - offset) + shape_copy.apply_transformation(householder_matrix, -offset, offset) return shape_copy From 9bad6addda15719fa50062bf87659c5f95e40188 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Wed, 8 Jan 2020 10:39:09 +0100 Subject: [PATCH 036/177] Replace copy_and_transform functions --- mypackage/geometry.py | 91 +++++++++++++++++++++++++----------------- tests/test_geometry.py | 74 +++++++++++++++++----------------- 2 files changed, 93 insertions(+), 72 deletions(-) diff --git a/mypackage/geometry.py b/mypackage/geometry.py index 86eaaab..96f4b9b 100644 --- a/mypackage/geometry.py +++ b/mypackage/geometry.py @@ -1,7 +1,6 @@ """Provides classes to define lines and surfaces.""" import numpy as np -import copy from scipy.spatial.transform import Rotation as R @@ -54,22 +53,21 @@ def check_valid(self, point_start, point_end): :return: --- """ - def copy_and_transform(self, - _unused_transformation_matrix=np.array( - [[1, 0], [0, 1]]), - _unused_translation_pre=np.array([0, 0]), - _unused_translation_post=np.array([0, 0])): + def apply_transformation(self, + _unused_transformation_matrix=np.array( + [[1, 0], [0, 1]]), + _unused_translation_pre=np.array([0, 0]), + _unused_translation_post=np.array([0, 0])): """ - Create a transformed copy of the segment. + Apply a transformation to the segment. :param _unused_transformation_matrix: Transformation matrix :param _unused_translation_pre: Translation applied before the matrix multiplication. :param _unused_translation_post: Translation applied after the matrix multiplication. - :return: Transformed copy + :return: --- """ - return copy.deepcopy(self) class LineSegment(Segment): """Line segment.""" @@ -113,7 +111,7 @@ def __init__(self, center, winding_ccw=True): :param point_center: Center point of the arc """ - point_center = np.array(center) + point_center = np.array(center, dtype=float) check_point_data_valid(point_center) self._point_center = point_center @@ -223,28 +221,30 @@ def check_valid(self, point_start, point_end): "Segment start and end points are not compatible with " "given center of the arc.") - def copy_and_transform(self, - transformation_matrix=np.array( - [[1, 0], [0, 1]]), - translation_pre=np.array([0, 0]), - translation_post=np.array([0, 0])): + def apply_transformation(self, + transformation_matrix=np.array( + [[1, 0], [0, 1]]), + translation_pre=np.array([0, 0]), + translation_post=np.array([0, 0])): """ - Create a transformed copy of the segment. + Apply a transformation to the segment. :param transformation_matrix: Transformation matrix - :param translation_pre: Translation applied before the matrix + :param translation_pre: Translation applied before + the matrix multiplication. - :param translation_post: Translation applied after the matrix + :param translation_post: Translation applied after + the matrix multiplication. - :return: Transformed copy + :return: --- """ - point_center_copy = np.matmul(transformation_matrix, - self._point_center + - translation_pre) + translation_post + self._point_center += translation_pre + self._point_center = np.matmul(transformation_matrix, + self._point_center) + self._point_center += translation_post - winding_ccw_new = self._sign_winding < 0 - - return Shape2D.ArcSegment(point_center_copy, winding_ccw_new) + self._sign_winding *= Shape2D._reflection_multiplier( + transformation_matrix) def rasterize(self, raster_width, point_start, point_end): """ @@ -328,6 +328,28 @@ def _check_segment_type_valid(segment): if not isinstance(segment, Shape2D.Segment): raise TypeError("Invalid segment type") + @staticmethod + def _reflection_multiplier(transformation_matrix): + """ + Get a multiplier indicating if the transformation is a reflection. + + Returns -1 if the transformation contains a reflection and 1 if not. + + :param transformation_matrix: Transformation matrix + :return: 1 or -1 (see description) + """ + points = np.identity(2) + transformed_points = np.matmul(points, + np.transpose(transformation_matrix)) + determinant = np.linalg.det( + [transformed_points[1] - transformed_points[0], + -transformed_points[0]]) + + if determinant == 0: + raise Exception("Invalid transformation") + + return np.sign(determinant) + # Public methods ---------------------------------------------------------- def add_segment(self, point, segment=LineSegment()): @@ -353,7 +375,7 @@ def apply_transformation(self, translation_pre=np.array([0, 0]), translation_post=np.array([0, 0])): """ - Apply a transformation to the segment. + Apply a transformation to the shape. :param transformation_matrix: Transformation matrix :param translation_pre: Translation applied before the matrix @@ -369,18 +391,18 @@ def apply_transformation(self, self._points += translation_post for i in range(self.num_segments()): - self._segments[i] = self._segments[ - i].copy_and_transform(transformation_matrix, translation_pre, - translation_post) + self._segments[i].apply_transformation(transformation_matrix, + translation_pre, + translation_post) - def copy_and_reflect(self, reflection_normal, distance_to_origin=0): + def reflect(self, reflection_normal, distance_to_origin=0): """ - Create a copy of the shape and reflect it at a given axis. + Apply a reflection at the given axis to the shape. :param reflection_normal: Normal of the reflection axis :param distance_to_origin: Distance of the reflection axis to the origin - :return: Reflected copy of the shape + :return: --- """ dot_product = np.dot(reflection_normal, reflection_normal) outer_product = np.outer(reflection_normal, reflection_normal) @@ -389,10 +411,7 @@ def copy_and_reflect(self, reflection_normal, distance_to_origin=0): offset = np.array(reflection_normal) / np.sqrt( dot_product) * distance_to_origin - shape_copy = copy.deepcopy(self) - shape_copy.apply_transformation(householder_matrix, -offset, offset) - - return shape_copy + self.apply_transformation(householder_matrix, -offset, offset) def is_point_included(self, point): """ diff --git a/tests/test_geometry.py b/tests/test_geometry.py index 7150f61..c185980 100644 --- a/tests/test_geometry.py +++ b/tests/test_geometry.py @@ -1,6 +1,7 @@ import pytest import mypackage.geometry as geo import numpy as np +import copy def test_shape2d_construction(): @@ -233,41 +234,41 @@ def test_shape2d_rasterization(): assert np.abs(raster_width_eff - raster_width) < 0.1 * raster_width -def test_line_segment_copy_and_reflect(): - reflection_matrix = [[0, 1], [1, 0]] - - segment = geo.Shape2D.LineSegment() - segment_copy = segment.copy_and_transform(reflection_matrix) - - # ensure that segment_copy is a deepcopy of segment - assert segment_copy is not segment - - -def test_arc_segment_copy_and_transform(): - reflection_matrix = np.array([[0, 1], [1, 0]]) - offset = np.array([2, -1]) +def test_arc_segment_apply_transform(): + # create arc segment point_center = [2, 3] - segment = geo.Shape2D.ArcSegment(point_center) - segment_copy = segment.copy_and_transform(reflection_matrix, -offset, - offset) - segment_copy2 = segment_copy.copy_and_transform(reflection_matrix, -offset, - offset) - # ensure that segment_copy is a deepcopy of segment - assert segment_copy is not segment - assert segment_copy2 is not segment - assert segment_copy is not segment_copy2 + # check transformation with reflection + reflection_matrix = np.array([[0, 1], [1, 0]]) + translation_pre = np.array([-1, 1]) + translation_post = np.array([2, 1]) + segment_copy = copy.deepcopy(segment) + segment_copy.apply_transformation(reflection_matrix, translation_pre, + translation_post) # Check if new center point is correct assert segment_copy._point_center[0] == 6 - assert segment_copy._point_center[1] == -1 - assert segment_copy2._point_center[0] == point_center[0] - assert segment_copy2._point_center[1] == point_center[1] + assert segment_copy._point_center[1] == 2 # Check that winding order is changed assert segment_copy._sign_winding == segment._sign_winding * -1 - assert segment_copy2._sign_winding == segment._sign_winding + + # check transformation without reflection + rotation_matrix = np.array([[0, 1], [-1, 0]]) + translation_pre = np.array([3, -2]) + translation_post = np.array([-3, -3]) + + segment_copy = copy.deepcopy(segment) + segment_copy.apply_transformation(rotation_matrix, translation_pre, + translation_post) + + # Check if new center point is correct + assert segment_copy._point_center[0] == -2 + assert segment_copy._point_center[1] == -8 + + # Check that winding order is NOT changed + assert segment_copy._sign_winding == segment._sign_winding def check_reflected_point(point, reflected_point, axis_offset, @@ -280,7 +281,7 @@ def check_reflected_point(point, reflected_point, axis_offset, assert np.abs(determinant) < 1E-8 -def shape2d_copy_and_reflect_testcase(normal, distance_to_origin): +def shape2d_reflect_testcase(normal, distance_to_origin): points = np.array([[3, 4], [5, 0], [11, 3]]) @@ -297,7 +298,8 @@ def shape2d_copy_and_reflect_testcase(normal, distance_to_origin): shape.add_segment(points[2]) # create reflected shape - shape_reflected = shape.copy_and_reflect(normal, distance_to_origin) + shape_reflected = copy.deepcopy(shape) + shape_reflected.reflect(normal, distance_to_origin) # check reflected points check_reflected_point(point_center, @@ -309,11 +311,11 @@ def shape2d_copy_and_reflect_testcase(normal, distance_to_origin): offset, direction_reflection_axis) -def test_shape2d_copy_and_reflect(): - shape2d_copy_and_reflect_testcase([2, 1], np.linalg.norm([2, 1])) - shape2d_copy_and_reflect_testcase([0, 1], 5) - shape2d_copy_and_reflect_testcase([1, 0], 3) - shape2d_copy_and_reflect_testcase([1, 0], -3) - shape2d_copy_and_reflect_testcase([-7, 2], 4.12) - shape2d_copy_and_reflect_testcase([-7, -2], 4.12) - shape2d_copy_and_reflect_testcase([7, -2], 4.12) +def test_shape2d_reflect(): + shape2d_reflect_testcase([2, 1], np.linalg.norm([2, 1])) + shape2d_reflect_testcase([0, 1], 5) + shape2d_reflect_testcase([1, 0], 3) + shape2d_reflect_testcase([1, 0], -3) + shape2d_reflect_testcase([-7, 2], 4.12) + shape2d_reflect_testcase([-7, -2], 4.12) + shape2d_reflect_testcase([7, -2], 4.12) From a4f7abeeb5e7de80649b4213b6d45e8e37592063 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Wed, 8 Jan 2020 10:42:45 +0100 Subject: [PATCH 037/177] Fix deepsource issue --- mypackage/geometry.py | 47 +++++++++++++++++++++---------------------- 1 file changed, 23 insertions(+), 24 deletions(-) diff --git a/mypackage/geometry.py b/mypackage/geometry.py index 96f4b9b..d86d77c 100644 --- a/mypackage/geometry.py +++ b/mypackage/geometry.py @@ -28,6 +28,28 @@ def is_row_in_array(row, array): return (array == row).all(axis=1).any() +def reflection_multiplier(transformation_matrix): + """ + Get a multiplier indicating if the transformation is a reflection. + + Returns -1 if the transformation contains a reflection and 1 if not. + + :param transformation_matrix: Transformation matrix + :return: 1 or -1 (see description) + """ + points = np.identity(2) + transformed_points = np.matmul(points, + np.transpose(transformation_matrix)) + determinant = np.linalg.det( + [transformed_points[1] - transformed_points[0], + -transformed_points[0]]) + + if determinant == 0: + raise Exception("Invalid transformation") + + return np.sign(determinant) + + class Shape2D: """Defines a shape in 2 dimensions.""" @@ -243,8 +265,7 @@ def apply_transformation(self, self._point_center) self._point_center += translation_post - self._sign_winding *= Shape2D._reflection_multiplier( - transformation_matrix) + self._sign_winding *= reflection_multiplier(transformation_matrix) def rasterize(self, raster_width, point_start, point_end): """ @@ -328,28 +349,6 @@ def _check_segment_type_valid(segment): if not isinstance(segment, Shape2D.Segment): raise TypeError("Invalid segment type") - @staticmethod - def _reflection_multiplier(transformation_matrix): - """ - Get a multiplier indicating if the transformation is a reflection. - - Returns -1 if the transformation contains a reflection and 1 if not. - - :param transformation_matrix: Transformation matrix - :return: 1 or -1 (see description) - """ - points = np.identity(2) - transformed_points = np.matmul(points, - np.transpose(transformation_matrix)) - determinant = np.linalg.det( - [transformed_points[1] - transformed_points[0], - -transformed_points[0]]) - - if determinant == 0: - raise Exception("Invalid transformation") - - return np.sign(determinant) - # Public methods ---------------------------------------------------------- def add_segment(self, point, segment=LineSegment()): From 5892c35cec7580b94cd89bfc4700ceb68525ea68 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Wed, 8 Jan 2020 11:28:19 +0100 Subject: [PATCH 038/177] Modify .codecov.yml --- .codecov.yml | 50 +++++++++++++++++++++++++------------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/.codecov.yml b/.codecov.yml index be5f693..94fc897 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -1,35 +1,35 @@ codecov: - branch: master - notify: - require_ci_to_pass: yes + branch: master + notify: + require_ci_to_pass: yes coverage: - precision: 2 - round: down - range: "70...100" + precision: 2 + round: down + range: "70...100" - status: - project: - default: - enabled: yes - target: 85% - threshold: 5% + status: + project: + default: + enabled: yes + target: 85% + threshold: 5% - patch: - default: - enabled: yes - target: 85% + patch: + default: + enabled: yes + target: 85% ignore: - - "tests" - - "*__init__.py" + - "tests" + - "*__init__.py" comment: - layout: "reach, diff, flags, files" - behavior: default - require_changes: false # if true: only post the comment if coverage changes - require_base: no # [yes :: must have a base report to post] - require_head: yes # [yes :: must have a head report to post] - branches: null # branch names that can post comment - after_n_builds: #e.g., 5. The number of uploaded reports codecov will receive before posting a comment on a pull request. + layout: "reach, diff, flags, files" + behavior: default + require_changes: false # if true: only post the comment if coverage changes + #require_base: no # [yes :: must have a base report to post] + #require_head: yes # [yes :: must have a head report to post] + #branches: null # branch names that can post comment + after_n_builds: 1 #e.g., 5. The number of uploaded reports codecov will receive before posting a comment on a pull request. From dddb35820db4ba44c4829d2d6e48386324aa9f4e Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Wed, 8 Jan 2020 12:22:55 +0100 Subject: [PATCH 039/177] Modify .codecov.yml --- .codecov.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.codecov.yml b/.codecov.yml index 94fc897..74e9364 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -20,6 +20,7 @@ coverage: default: enabled: yes target: 85% + threshold: 5% ignore: - "tests" From 9e65fb0fa2301bc952e978db34a4e6ca9afb30ca Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Wed, 8 Jan 2020 12:33:47 +0100 Subject: [PATCH 040/177] Modify codecov.yml --- .codecov.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.codecov.yml b/.codecov.yml index 74e9364..c371f9b 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -26,11 +26,11 @@ ignore: - "tests" - "*__init__.py" -comment: - layout: "reach, diff, flags, files" - behavior: default - require_changes: false # if true: only post the comment if coverage changes + #comment: + #layout: "reach, diff, flags, files" + #behavior: default + #require_changes: false # if true: only post the comment if coverage changes #require_base: no # [yes :: must have a base report to post] #require_head: yes # [yes :: must have a head report to post] #branches: null # branch names that can post comment - after_n_builds: 1 #e.g., 5. The number of uploaded reports codecov will receive before posting a comment on a pull request. + #after_n_builds: 1 #e.g., 5. The number of uploaded reports codecov will receive before posting a comment on a pull request. From 27aabbc3be7faa2de15bcb0f40b9e05bf406e2f7 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Wed, 8 Jan 2020 12:47:17 +0100 Subject: [PATCH 041/177] Rename .codecov.yml -> codecov.yml --- .codecov.yml => codecov.yml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .codecov.yml => codecov.yml (100%) diff --git a/.codecov.yml b/codecov.yml similarity index 100% rename from .codecov.yml rename to codecov.yml From 7cf828e4fa8105380c226431d344dbd485e95092 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Wed, 8 Jan 2020 14:45:19 +0100 Subject: [PATCH 042/177] Add some utility functions --- mypackage/geometry.py | 69 +++++++++++++++++++++++++++++++----------- tests/test_geometry.py | 11 ++++--- 2 files changed, 58 insertions(+), 22 deletions(-) diff --git a/mypackage/geometry.py b/mypackage/geometry.py index d86d77c..143a179 100644 --- a/mypackage/geometry.py +++ b/mypackage/geometry.py @@ -28,6 +28,39 @@ def is_row_in_array(row, array): return (array == row).all(axis=1).any() +def vector_points_to_left_of_vector(vector, vector_reference): + """ + Determine if a vector points to the left of another vector. + + Returns 1 if the vector points to the left of the reference vector and + -1 if it points to the right. In case both vectors point into the same + or the opposite directions, this function returns 0. + + :param vector: Vector + :param vector_reference: Reference vector + :return: 1,-1 or 0 (see description) + """ + return np.sign(np.linalg.det([vector_reference, vector])) + + +def point_left_of_line(point, line_start, line_end): + """ + Determine if a point lies left of a line. + + Returns 1 if the point is left of the line and -1 if it is to the right. + If the point is located on the line, this function returns 0. + + :param point: Point + :param line_start: Starting point of the line + :param line_end: End point of the line + :return: 1,-1 or 0 (see description) + """ + vec_line_start_end = line_end - line_start + vec_line_start_point = point - line_start + return vector_points_to_left_of_vector(vec_line_start_point, + vec_line_start_end) + + def reflection_multiplier(transformation_matrix): """ Get a multiplier indicating if the transformation is a reflection. @@ -40,14 +73,14 @@ def reflection_multiplier(transformation_matrix): points = np.identity(2) transformed_points = np.matmul(points, np.transpose(transformation_matrix)) - determinant = np.linalg.det( - [transformed_points[1] - transformed_points[0], - -transformed_points[0]]) + origin_left_of_line = point_left_of_line(np.array([0, 0]), + transformed_points[0], + transformed_points[1]) - if determinant == 0: + if origin_left_of_line == 0: raise Exception("Invalid transformation") - return np.sign(determinant) + return np.sign(origin_left_of_line) class Shape2D: @@ -127,20 +160,22 @@ def rasterize(raster_width, point_start, point_end): class ArcSegment(Segment): """Segment of a circle.""" - def __init__(self, center, winding_ccw=True): + def __init__(self, point_center, arc_winding_ccw=True): """ Constructor. :param point_center: Center point of the arc + :param: arc_winding_ccw: Specifies if the arcs winding order is + counter-clockwise """ - point_center = np.array(center, dtype=float) + point_center = np.array(point_center, dtype=float) check_point_data_valid(point_center) self._point_center = point_center - if winding_ccw: - self._sign_winding = 1 + if arc_winding_ccw: + self._sign_arc_winding = 1 else: - self._sign_winding = -1 + self._sign_arc_winding = -1 def _arc_angle_and_length(self, vec_center_start, vec_center_end): """ @@ -151,7 +186,7 @@ def _arc_angle_and_length(self, vec_center_start, vec_center_end): :param vec_center_end: Vector from the arcs center to the end point :return: Array containing the arcs angle and arc length """ - sign_winding = self._sign_winding + sign_arc_winding = self._sign_arc_winding radius = np.linalg.norm(vec_center_start) # Calculate angle between vectors (always the smaller one) @@ -161,11 +196,10 @@ def _arc_angle_and_length(self, vec_center_start, vec_center_end): dot_unit = np.dot(unit_center_start, unit_center_end) angle_vecs = np.arccos(np.clip(dot_unit, -1, 1)) - # If the determinant is < 0 point_end is on the right of - # vec_center_start. Otherwise it is on the left-hand side. - determinant = np.linalg.det([vec_center_start, vec_center_end]) + sign_winding_points = vector_points_to_left_of_vector( + vec_center_end, vec_center_start) - if np.abs(np.sign(determinant) + sign_winding) > 0: + if np.abs(sign_winding_points + sign_arc_winding) > 0: arc_angle = angle_vecs else: arc_angle = 2 * np.pi - angle_vecs @@ -187,7 +221,7 @@ def _rotation_angles(self, vec_center_start, vec_center_end, :param raster_width: Desired raster width :return: Array containing the rotation angles """ - sign = self._sign_winding + sign = self._sign_arc_winding [angle_arc, arc_length] = self._arc_angle_and_length( vec_center_start, vec_center_end) @@ -265,7 +299,8 @@ def apply_transformation(self, self._point_center) self._point_center += translation_post - self._sign_winding *= reflection_multiplier(transformation_matrix) + self._sign_arc_winding *= reflection_multiplier( + transformation_matrix) def rasterize(self, raster_width, point_start, point_end): """ diff --git a/tests/test_geometry.py b/tests/test_geometry.py index c185980..0ae5b90 100644 --- a/tests/test_geometry.py +++ b/tests/test_geometry.py @@ -115,14 +115,15 @@ def test_line_segment_rasterizaion(): def arc_segment_test(point_center, point_start, point_end, raster_width, - winding_ccw, check_winding): + arc_winding_ccw, check_winding): point_center = np.array(point_center) point_start = np.array(point_start) point_end = np.array(point_end) radius_arc = np.linalg.norm(point_start - point_center) - arc_segment = geo.Shape2D.ArcSegment(point_center, winding_ccw=winding_ccw) + arc_segment = geo.Shape2D.ArcSegment(point_center, + arc_winding_ccw=arc_winding_ccw) arc_segment.check_valid(point_start, point_end) data = arc_segment.rasterize(raster_width, point_start, point_end) @@ -234,7 +235,7 @@ def test_shape2d_rasterization(): assert np.abs(raster_width_eff - raster_width) < 0.1 * raster_width -def test_arc_segment_apply_transform(): +def test_arc_segment_apply_transformation(): # create arc segment point_center = [2, 3] segment = geo.Shape2D.ArcSegment(point_center) @@ -252,7 +253,7 @@ def test_arc_segment_apply_transform(): assert segment_copy._point_center[1] == 2 # Check that winding order is changed - assert segment_copy._sign_winding == segment._sign_winding * -1 + assert segment_copy._sign_arc_winding == segment._sign_arc_winding * -1 # check transformation without reflection rotation_matrix = np.array([[0, 1], [-1, 0]]) @@ -268,7 +269,7 @@ def test_arc_segment_apply_transform(): assert segment_copy._point_center[1] == -8 # Check that winding order is NOT changed - assert segment_copy._sign_winding == segment._sign_winding + assert segment_copy._sign_arc_winding == segment._sign_arc_winding def check_reflected_point(point, reflected_point, axis_offset, From 118435e141df43257e9cc8ed68f9da4a51eb74da Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Wed, 8 Jan 2020 15:44:56 +0100 Subject: [PATCH 043/177] Add tests --- tests/test_geometry.py | 60 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/tests/test_geometry.py b/tests/test_geometry.py index 0ae5b90..ea31736 100644 --- a/tests/test_geometry.py +++ b/tests/test_geometry.py @@ -4,6 +4,66 @@ import copy +def test_vector_points_to_left_of_vector(): + assert geo.vector_points_to_left_of_vector([-0.1, 1], [0, 1]) > 0 + assert geo.vector_points_to_left_of_vector([-0.1, -1], [0, 1]) > 0 + assert geo.vector_points_to_left_of_vector([3, 5], [1, 0]) > 0 + assert geo.vector_points_to_left_of_vector([-3, 5], [1, 0]) > 0 + assert geo.vector_points_to_left_of_vector([0, -0.1], [-4, 2]) > 0 + assert geo.vector_points_to_left_of_vector([-1, -0.1], [-4, 2]) > 0 + + assert geo.vector_points_to_left_of_vector([0.1, 1], [0, 1]) < 0 + assert geo.vector_points_to_left_of_vector([0.1, -1], [0, 1]) < 0 + assert geo.vector_points_to_left_of_vector([3, -5], [1, 0]) < 0 + assert geo.vector_points_to_left_of_vector([-3, -5], [1, 0]) < 0 + assert geo.vector_points_to_left_of_vector([0, 0.1], [-4, 2]) < 0 + assert geo.vector_points_to_left_of_vector([1, -0.1], [-4, 2]) < 0 + + assert geo.vector_points_to_left_of_vector([4, 4], [2, 2]) == 0 + assert geo.vector_points_to_left_of_vector([-4, -4], [2, 2]) == 0 + + +def test_point_left_of_line(): + line_start = np.array([2, 3]) + line_end = np.array([5, 6]) + assert geo.point_left_of_line([-8, 10], line_start, line_end) > 0 + assert geo.point_left_of_line([3, 0], line_start, line_end) < 0 + assert geo.point_left_of_line(line_start, line_start, line_end) == 0 + + line_start = np.array([2, 3]) + line_end = np.array([1, -4]) + assert geo.point_left_of_line([3, 0], line_start, line_end) > 0 + assert geo.point_left_of_line([-8, 10], line_start, line_end) < 0 + assert geo.point_left_of_line(line_start, line_start, line_end) == 0 + + +def test_reflection_multiplier(): + assert geo.reflection_multiplier([[-1, 0], [0, 1]]) == -1 + assert geo.reflection_multiplier([[1, 0], [0, -1]]) == -1 + assert geo.reflection_multiplier([[0, 1], [1, 0]]) == -1 + assert geo.reflection_multiplier([[0, -1], [-1, 0]]) == -1 + assert geo.reflection_multiplier([[-4, 0], [0, 2]]) == -1 + assert geo.reflection_multiplier([[6, 0], [0, -4]]) == -1 + assert geo.reflection_multiplier([[0, 3], [8, 0]]) == -1 + assert geo.reflection_multiplier([[0, -3], [-2, 0]]) == -1 + + assert geo.reflection_multiplier([[1, 0], [0, 1]]) == 1 + assert geo.reflection_multiplier([[-1, 0], [0, -1]]) == 1 + assert geo.reflection_multiplier([[0, -1], [1, 0]]) == 1 + assert geo.reflection_multiplier([[0, 1], [-1, 0]]) == 1 + assert geo.reflection_multiplier([[5, 0], [0, 6]]) == 1 + assert geo.reflection_multiplier([[-3, 0], [0, -7]]) == 1 + assert geo.reflection_multiplier([[0, -8], [9, 0]]) == 1 + assert geo.reflection_multiplier([[0, 3], [-2, 0]]) == 1 + + with pytest.raises(Exception): + geo.reflection_multiplier([[0, 0], [0, 0]]) + with pytest.raises(Exception): + geo.reflection_multiplier([[1, 0], [0, 0]]) + with pytest.raises(Exception): + geo.reflection_multiplier([[2, 2], [1, 1]]) + + def test_shape2d_construction(): # Test Exception: Segment length too small with pytest.raises(Exception): From 0071ec62dabc9dead91e9a83fef6fbdeb6bf5482 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Wed, 8 Jan 2020 16:22:15 +0100 Subject: [PATCH 044/177] Add function to add a shape to a profile --- mypackage/point_cloud_generator.py | 19 +++++++++++++++++++ tests/test_point_cloud_generator.py | 16 +++++++++++----- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/mypackage/point_cloud_generator.py b/mypackage/point_cloud_generator.py index 57fdbbd..11c11b4 100644 --- a/mypackage/point_cloud_generator.py +++ b/mypackage/point_cloud_generator.py @@ -20,3 +20,22 @@ def __init__(self, shapes): "Only instances or lists of Shape2d objects are accepted.") self.shapes = shapes + + def add_shape(self, shape): + """ + Add a shape to the profile. + + :param shape: Shape that should be added. + :return: --- + """ + if not isinstance(shape, geo.Shape2D): + raise ValueError("Only instances Shape2d objects are accepted.") + self.shapes.append(shape) + + def num_shapes(self): + """ + Get the number of shapes of the profile. + + :return: Number of shapes + """ + return len(self.shapes) diff --git a/tests/test_point_cloud_generator.py b/tests/test_point_cloud_generator.py index 3d516b0..78b1d59 100644 --- a/tests/test_point_cloud_generator.py +++ b/tests/test_point_cloud_generator.py @@ -3,15 +3,11 @@ import mypackage.geometry as geo -def test_profile_construction(): +def test_profile_construction_and_shape_addition(): shape = geo.Shape2D([0, 0], [1, 0]) shape.add_segment([2, -1]) shape.add_segment([0, -1]) - # Check valid types - pcg.Profile(shape) - pcg.Profile([shape, shape]) - # Check invalid types with pytest.raises(ValueError): pcg.Profile(3) @@ -19,3 +15,13 @@ def test_profile_construction(): pcg.Profile("This is not right") with pytest.raises(ValueError): pcg.Profile([2, 8, 1]) + + # Check valid types + profile = pcg.Profile(shape) + assert profile.num_shapes() == 1 + + profile = pcg.Profile([shape, shape]) + assert profile.num_shapes() == 2 + + profile.add_shape(shape) + assert profile.num_shapes() == 3 From 8c2a60fc4818e73196360c2812f9b6a78689948b Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Wed, 8 Jan 2020 17:04:58 +0100 Subject: [PATCH 045/177] Separate translations from matrix transformations --- mypackage/geometry.py | 78 ++++++++++++++++++++---------------------- tests/test_geometry.py | 34 +++++++++++------- 2 files changed, 60 insertions(+), 52 deletions(-) diff --git a/mypackage/geometry.py b/mypackage/geometry.py index 143a179..d2ceaa3 100644 --- a/mypackage/geometry.py +++ b/mypackage/geometry.py @@ -108,19 +108,19 @@ def check_valid(self, point_start, point_end): :return: --- """ - def apply_transformation(self, - _unused_transformation_matrix=np.array( - [[1, 0], [0, 1]]), - _unused_translation_pre=np.array([0, 0]), - _unused_translation_post=np.array([0, 0])): + def translate(self, _unused_vector): + """ + Apply a translation to the segment + + :param _unused_vector: Translation vector + :return: --- + """ + + def apply_transformation(self, _unused_transformation_matrix): """ Apply a transformation to the segment. :param _unused_transformation_matrix: Transformation matrix - :param _unused_translation_pre: Translation applied before the - matrix multiplication. - :param _unused_translation_post: Translation applied after the - matrix multiplication. :return: --- """ @@ -277,28 +277,16 @@ def check_valid(self, point_start, point_end): "Segment start and end points are not compatible with " "given center of the arc.") - def apply_transformation(self, - transformation_matrix=np.array( - [[1, 0], [0, 1]]), - translation_pre=np.array([0, 0]), - translation_post=np.array([0, 0])): + def apply_transformation(self, transformation_matrix): """ Apply a transformation to the segment. :param transformation_matrix: Transformation matrix - :param translation_pre: Translation applied before - the matrix - multiplication. - :param translation_post: Translation applied after - the matrix - multiplication. :return: --- """ - self._point_center += translation_pre + self._point_center = np.matmul(transformation_matrix, self._point_center) - self._point_center += translation_post - self._sign_arc_winding *= reflection_multiplier( transformation_matrix) @@ -326,8 +314,14 @@ def rasterize(self, raster_width, point_start, point_end): return self._rasterize(vec_center_start, rotation_angles) - # Private methods - # --------------------------------------------------------- + def translate(self, vector): + + """ + Apply a translation to the segment + :param vector: Translation vector + :return: --- + """ + self._point_center += vector def __init__(self, point0, point1, segment=LineSegment()): """ @@ -345,6 +339,8 @@ def __init__(self, point0, point1, segment=LineSegment()): self._points = np.array([point0, point1], dtype=float) self._segments = [segment] + # Private methods --------------------------------------------------------- + @staticmethod def _check_segment(segment, point_start, point_end): """ @@ -404,30 +400,18 @@ def add_segment(self, point, segment=LineSegment()): self._points = np.vstack((self._points, point)) self._segments.append(segment) - def apply_transformation(self, - transformation_matrix=np.array([[1, 0], [0, 1]]), - translation_pre=np.array([0, 0]), - translation_post=np.array([0, 0])): + def apply_transformation(self, transformation_matrix): """ Apply a transformation to the shape. :param transformation_matrix: Transformation matrix - :param translation_pre: Translation applied before the matrix - multiplication. - :param translation_post: Translation applied after the matrix - multiplication. :return: --- """ - self._points += translation_pre self._points = np.matmul(self._points, np.transpose(transformation_matrix)) - self._points += translation_post - for i in range(self.num_segments()): - self._segments[i].apply_transformation(transformation_matrix, - translation_pre, - translation_post) + self._segments[i].apply_transformation(transformation_matrix) def reflect(self, reflection_normal, distance_to_origin=0): """ @@ -445,7 +429,21 @@ def reflect(self, reflection_normal, distance_to_origin=0): offset = np.array(reflection_normal) / np.sqrt( dot_product) * distance_to_origin - self.apply_transformation(householder_matrix, -offset, offset) + self.translate(-offset) + self.apply_transformation(householder_matrix) + self.translate(offset) + + def translate(self, vector): + """ + Apply a translation to the shape. + + :param vector: Translation vector + :return: --- + """ + self._points += vector + + for i in range(self.num_segments()): + self._segments[i].translate(vector) def is_point_included(self, point): """ diff --git a/tests/test_geometry.py b/tests/test_geometry.py index ea31736..465a656 100644 --- a/tests/test_geometry.py +++ b/tests/test_geometry.py @@ -295,7 +295,7 @@ def test_shape2d_rasterization(): assert np.abs(raster_width_eff - raster_width) < 0.1 * raster_width -def test_arc_segment_apply_transformation(): +def test_arc_segment_transformations(): # create arc segment point_center = [2, 3] segment = geo.Shape2D.ArcSegment(point_center) @@ -305,32 +305,42 @@ def test_arc_segment_apply_transformation(): translation_pre = np.array([-1, 1]) translation_post = np.array([2, 1]) segment_copy = copy.deepcopy(segment) - segment_copy.apply_transformation(reflection_matrix, translation_pre, - translation_post) + segment_copy.translate(translation_pre) + assert segment_copy._point_center[0] == 1 + assert segment_copy._point_center[1] == 4 + + segment_copy.apply_transformation(reflection_matrix) + assert segment_copy._point_center[0] == 4 + assert segment_copy._point_center[1] == 1 + # Check that winding order is NOT changed + assert segment_copy._sign_arc_winding == segment._sign_arc_winding * -1 + + segment_copy.translate(translation_post) # Check if new center point is correct assert segment_copy._point_center[0] == 6 assert segment_copy._point_center[1] == 2 - # Check that winding order is changed - assert segment_copy._sign_arc_winding == segment._sign_arc_winding * -1 - # check transformation without reflection rotation_matrix = np.array([[0, 1], [-1, 0]]) translation_pre = np.array([3, -2]) translation_post = np.array([-3, -3]) segment_copy = copy.deepcopy(segment) - segment_copy.apply_transformation(rotation_matrix, translation_pre, - translation_post) - - # Check if new center point is correct - assert segment_copy._point_center[0] == -2 - assert segment_copy._point_center[1] == -8 + segment_copy.translate(translation_pre) + assert segment_copy._point_center[0] == 5 + assert segment_copy._point_center[1] == 1 + segment_copy.apply_transformation(rotation_matrix) + assert segment_copy._point_center[0] == 1 + assert segment_copy._point_center[1] == -5 # Check that winding order is NOT changed assert segment_copy._sign_arc_winding == segment._sign_arc_winding + segment_copy.translate(translation_post) + assert segment_copy._point_center[0] == -2 + assert segment_copy._point_center[1] == -8 + def check_reflected_point(point, reflected_point, axis_offset, direction_reflection_axis): From 451d600242cc7554f383b0c0b3ccebcd8cf0268d Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Wed, 8 Jan 2020 17:07:20 +0100 Subject: [PATCH 046/177] Fix docstrings --- mypackage/geometry.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/mypackage/geometry.py b/mypackage/geometry.py index d2ceaa3..75a853c 100644 --- a/mypackage/geometry.py +++ b/mypackage/geometry.py @@ -110,7 +110,7 @@ def check_valid(self, point_start, point_end): def translate(self, _unused_vector): """ - Apply a translation to the segment + Apply a translation to the segment. :param _unused_vector: Translation vector :return: --- @@ -284,7 +284,6 @@ def apply_transformation(self, transformation_matrix): :param transformation_matrix: Transformation matrix :return: --- """ - self._point_center = np.matmul(transformation_matrix, self._point_center) self._sign_arc_winding *= reflection_multiplier( @@ -315,9 +314,9 @@ def rasterize(self, raster_width, point_start, point_end): return self._rasterize(vec_center_start, rotation_angles) def translate(self, vector): - """ - Apply a translation to the segment + Apply a translation to the segment. + :param vector: Translation vector :return: --- """ From 67940738a14b5f4d8bdc43dcba3d26bd42687801 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Wed, 8 Jan 2020 17:51:05 +0100 Subject: [PATCH 047/177] Add missing shhape2d tests --- tests/test_geometry.py | 95 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 85 insertions(+), 10 deletions(-) diff --git a/tests/test_geometry.py b/tests/test_geometry.py index 465a656..af20750 100644 --- a/tests/test_geometry.py +++ b/tests/test_geometry.py @@ -342,6 +342,89 @@ def test_arc_segment_transformations(): assert segment_copy._point_center[1] == -8 +def default_test_shape(): + points = np.array([[3, 4], + [5, 0], + [11, 3]]) + point_center = np.array([6, 3]) + + # create shape + arc_segment = geo.Shape2D.ArcSegment(point_center) + shape = geo.Shape2D(points[0], points[1], arc_segment) + shape.add_segment(points[2]) + + return shape + + +def test_shape2d_translation(): + def check_point(point, point_ref, translation): + assert point[0] - point_ref[0] - translation[0] < 1E-9 + assert point[1] - point_ref[1] - translation[1] < 1E-9 + + translation = [3, 4] + + shape_ref = default_test_shape() + shape = copy.deepcopy(shape_ref) + + # apply translation + shape.translate(translation) + + for i in range(shape.num_points()): + check_point(shape._points[i], shape_ref._points[i], translation) + + arc_segment = shape._segments[0] + arc_segment_ref = shape_ref._segments[0] + + check_point(arc_segment._point_center, arc_segment_ref._point_center, + translation) + assert arc_segment._sign_arc_winding == arc_segment_ref._sign_arc_winding + + +def test_shape2d_transformation(): + # without reflection + def check_point_rotation(point, point_ref): + assert point[0] == point_ref[1] + assert point[1] == -point_ref[0] + + rotation_matrix = np.array([[0, 1], [-1, 0]]) + + shape_ref = default_test_shape() + shape = copy.deepcopy(shape_ref) + + # apply transformation + shape.apply_transformation(rotation_matrix) + + for i in range(shape.num_points()): + check_point_rotation(shape._points[i], shape_ref._points[i]) + + arc_segment = shape._segments[0] + arc_segment_ref = shape_ref._segments[0] + check_point_rotation(arc_segment._point_center, + arc_segment_ref._point_center) + assert arc_segment._sign_arc_winding == arc_segment_ref._sign_arc_winding + + # with reflection + def check_point_reflection(point, point_ref): + assert point[0] == point_ref[1] + assert point[1] == point_ref[0] + + reflection_matrix = np.array([[0, 1], [1, 0]]) + + shape = copy.deepcopy(shape_ref) + + # apply transformation + shape.apply_transformation(reflection_matrix) + + for i in range(shape.num_points()): + check_point_reflection(shape._points[i], shape_ref._points[i]) + + arc_segment = shape._segments[0] + arc_segment_ref = shape_ref._segments[0] + check_point_reflection(arc_segment._point_center, + arc_segment_ref._point_center) + assert arc_segment._sign_arc_winding == -arc_segment_ref._sign_arc_winding + + def check_reflected_point(point, reflected_point, axis_offset, direction_reflection_axis): """Check if the midpoint lies on the reflection axis.""" @@ -353,27 +436,19 @@ def check_reflected_point(point, reflected_point, axis_offset, def shape2d_reflect_testcase(normal, distance_to_origin): - points = np.array([[3, 4], - [5, 0], - [11, 3]]) - point_center = np.array([6, 3]) - direction_reflection_axis = np.array([normal[1], -normal[0]]) normal_length = np.linalg.norm(normal) unit_normal = np.array(normal) / normal_length offset = distance_to_origin * unit_normal - # create shape - arc_segment = geo.Shape2D.ArcSegment(point_center) - shape = geo.Shape2D(points[0], points[1], arc_segment) - shape.add_segment(points[2]) + shape = default_test_shape() # create reflected shape shape_reflected = copy.deepcopy(shape) shape_reflected.reflect(normal, distance_to_origin) # check reflected points - check_reflected_point(point_center, + check_reflected_point(shape._segments[0]._point_center, shape_reflected._segments[0]._point_center, offset, direction_reflection_axis) From 752659cb06e0e7bbdbdcefb058d13e852cadf47d Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Thu, 9 Jan 2020 10:56:40 +0100 Subject: [PATCH 048/177] Refactor profile class --- mypackage/point_cloud_generator.py | 41 ++++++++++++++++++++--------- tests/test_point_cloud_generator.py | 24 +++++++++++++---- 2 files changed, 47 insertions(+), 18 deletions(-) diff --git a/mypackage/point_cloud_generator.py b/mypackage/point_cloud_generator.py index 11c11b4..3fe659f 100644 --- a/mypackage/point_cloud_generator.py +++ b/mypackage/point_cloud_generator.py @@ -1,6 +1,7 @@ """Contains methods and classes to generate 3d point clouds.""" import mypackage.geometry as geo +import numpy as np class Profile: @@ -12,25 +13,25 @@ def __init__(self, shapes): :param: shapes: Instance or list of geo.Shape2D class(es) """ - if not isinstance(shapes, list): - shapes = [shapes] - - if not all(isinstance(shape, geo.Shape2D) for shape in shapes): - raise ValueError( - "Only instances or lists of Shape2d objects are accepted.") + self.shapes = [] - self.shapes = shapes + self.add_shapes(shapes) - def add_shape(self, shape): + def add_shapes(self, shapes): """ - Add a shape to the profile. + Add shapes to the profile. - :param shape: Shape that should be added. + :param shapes: Instance or list of geo.Shape2D class(es) :return: --- """ - if not isinstance(shape, geo.Shape2D): - raise ValueError("Only instances Shape2d objects are accepted.") - self.shapes.append(shape) + if not isinstance(shapes, list): + shapes = [shapes] + + if not all(isinstance(shape, geo.Shape2D) for shape in shapes): + raise TypeError( + "Only instances or lists of Shape2d objects are accepted.") + + self.shapes += shapes def num_shapes(self): """ @@ -39,3 +40,17 @@ def num_shapes(self): :return: Number of shapes """ return len(self.shapes) + + def rasterize(self, raster_width): + """ + Rasterize the profile. + + :param: raster_width: Raster width + :return: Raster data + """ + raster_data = np.empty([0, 3]) + for shape in self.shapes: + raster_data = np.vstack( + (raster_data, shape.rasterize(raster_width))) + + return raster_data diff --git a/tests/test_point_cloud_generator.py b/tests/test_point_cloud_generator.py index 78b1d59..619892f 100644 --- a/tests/test_point_cloud_generator.py +++ b/tests/test_point_cloud_generator.py @@ -9,19 +9,33 @@ def test_profile_construction_and_shape_addition(): shape.add_segment([0, -1]) # Check invalid types - with pytest.raises(ValueError): + with pytest.raises(TypeError): pcg.Profile(3) - with pytest.raises(ValueError): + with pytest.raises(TypeError): pcg.Profile("This is not right") - with pytest.raises(ValueError): + with pytest.raises(TypeError): pcg.Profile([2, 8, 1]) # Check valid types profile = pcg.Profile(shape) assert profile.num_shapes() == 1 - profile = pcg.Profile([shape, shape]) assert profile.num_shapes() == 2 - profile.add_shape(shape) + # Check invalid addition + with pytest.raises(TypeError): + profile.add_shapes([shape, 0.1]) + with pytest.raises(TypeError): + profile.add_shapes(["shape"]) + with pytest.raises(TypeError): + profile.add_shapes(0.1) + + # Check that invalid calls only raise an exception and do not invalidate + # the internal data + assert profile.num_shapes() == 2 + + # Check valid addition + profile.add_shapes(shape) assert profile.num_shapes() == 3 + profile.add_shapes([shape, shape]) + assert profile.num_shapes() == 5 From 9ed5b6090ef437516ddff95fd92e99eea17f685e Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Thu, 9 Jan 2020 11:29:35 +0100 Subject: [PATCH 049/177] Test profile rasterization --- tests/test_point_cloud_generator.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/test_point_cloud_generator.py b/tests/test_point_cloud_generator.py index 619892f..7cbdb8e 100644 --- a/tests/test_point_cloud_generator.py +++ b/tests/test_point_cloud_generator.py @@ -39,3 +39,27 @@ def test_profile_construction_and_shape_addition(): assert profile.num_shapes() == 3 profile.add_shapes([shape, shape]) assert profile.num_shapes() == 5 + + +def test_profile_rasterization(): + raster_width = 0.1 + shape0 = geo.Shape2D([-1, 0], [-raster_width, 0]) + shape1 = geo.Shape2D([0, 0], [1, 0]) + shape2 = geo.Shape2D([1 + raster_width, 0], [2, 0]) + + profile = pcg.Profile([shape0, shape1]) + profile.add_shapes(shape2) + + # rasterize + data = profile.rasterize(0.1) + + # check raster data size + expected_number_raster_points = int(round(3 / raster_width)) + 1 + assert len(data[:, 0]) == expected_number_raster_points + + # Check that all shapes are rasterized correct + for i in range(int(round(3 / raster_width)) + 1): + expected_raster_point_x = i * raster_width - 1 + assert data[i, 0] - expected_raster_point_x < 1E-9 + assert data[i, 1] == 0 + assert data[i, 2] == 0 From 2e6bfa41871854c36bdc29f64e3b2cbc0a617c82 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Fri, 10 Jan 2020 10:15:49 +0100 Subject: [PATCH 050/177] Start with 3d data --- mypackage/point_cloud_generator.py | 84 ++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/mypackage/point_cloud_generator.py b/mypackage/point_cloud_generator.py index 3fe659f..2ebeb1a 100644 --- a/mypackage/point_cloud_generator.py +++ b/mypackage/point_cloud_generator.py @@ -2,6 +2,7 @@ import mypackage.geometry as geo import numpy as np +from scipy.spatial.transform import Rotation as R class Profile: @@ -54,3 +55,86 @@ def rasterize(self, raster_width): (raster_data, shape.rasterize(raster_width))) return raster_data + + +def is_point_valid(point): + if not isinstance(point, np.ndarray): + return False + if not point.size == 3: + return False + return True + + +def is_orthogonal(u, v, tolerance=1E-9): + return np.abs(np.cross(u, v) - 1) < tolerance + + +class CartesianCoordinateSystem3d: + def __init__(self, x=None, y=None, z=None, positive_orientation=True): + if z is None: + if x is None or y is None: + raise Exception("Two axes need to be defined") + if not is_orthogonal(x, y): + raise Exception("Defined axes are not orthogonal") + + +def vector_to_vector_transformation(u, v): + r = np.cross(u, v) + w = np.sqrt(np.dot(u, u) * np.dot(v, v)) + np.dot(u, v) + quaternion = np.concatenate((r, [w])) + unit_quaternion = quaternion / np.linalg.norm(quaternion) + + return R.from_quat(unit_quaternion).as_dcm() + + +class LinearTrace: + + def __init__(self, point_start, point_end): + point_start = np.array(point_start) + point_end = np.array(point_end) + + if not is_point_valid(point_start): + raise TypeError("point_start is not a valid 3d point.") + if not is_point_valid(point_end): + raise TypeError("point_end is not a valid 3d point.") + + self._points = np.array([point_start, point_end]) + self._direction_vector = self._points[1] - self._points[0] + + def position(self, weight): + weight = np.clip(weight, 0, 1) + return self._points[0] + weight * self._direction_vector + + def direction(self, weight): + return self._direction_vector + + +class Section: + """Defines a section""" + + def __init__(self, profile_start, profile_end, trace): + + if not isinstance(profile_start, Profile): + raise TypeError("profile_start must be a Profile object.") + if not isinstance(profile_end, Profile): + raise TypeError("profile_end must be a Profile object.") + self._profiles = [profile_start, profile_end] + self.trace = trace + + def rasterize(self): + + raster_data = np.empty([0, 3]) + trace = self.trace + for weight in np.arange(0, 1.01, 0.1): + profile = self._profiles[0] + profile_raster_data = profile.rasterize(0.1) + + matrix = vector_to_vector_transformation([0, 0, -1], + trace.direction(weight)) + profile_raster_data = np.matmul(profile_raster_data, + np.transpose(matrix)) + profile_raster_data += trace.position(weight) + + raster_data = np.vstack((raster_data, profile_raster_data)) + + return raster_data From 06c18e60951de30c1848f16b0637bf5c08004999 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Fri, 10 Jan 2020 11:27:24 +0100 Subject: [PATCH 051/177] Modify CartesianCoordinateSystem class --- mypackage/point_cloud_generator.py | 49 ++++++++++++++++++++++++++---- 1 file changed, 43 insertions(+), 6 deletions(-) diff --git a/mypackage/point_cloud_generator.py b/mypackage/point_cloud_generator.py index 2ebeb1a..c6b5510 100644 --- a/mypackage/point_cloud_generator.py +++ b/mypackage/point_cloud_generator.py @@ -66,16 +66,53 @@ def is_point_valid(point): def is_orthogonal(u, v, tolerance=1E-9): - return np.abs(np.cross(u, v) - 1) < tolerance + return np.abs(np.dot(u, v)) < tolerance class CartesianCoordinateSystem3d: - def __init__(self, x=None, y=None, z=None, positive_orientation=True): + def __init__(self, x=None, y=None, z=None, origin=[0, 0, 0], + positive_orientation=True): + if positive_orientation: + sign_orientation = 1 + else: + sign_orientation = -1 + + if x is None: + x = self._calcualte_third_axis(y, z) * sign_orientation + if y is None: + y = self._calcualte_third_axis(z, x) * sign_orientation if z is None: - if x is None or y is None: - raise Exception("Two axes need to be defined") - if not is_orthogonal(x, y): - raise Exception("Defined axes are not orthogonal") + z = self._calcualte_third_axis(x, y) * sign_orientation + + e0 = x / np.linalg.norm(x) + e1 = y / np.linalg.norm(y) + e2 = z / np.linalg.norm(z) + self._basis = [e0, e1, e2] + self._origin = np.array(origin) + + @staticmethod + def _calcualte_third_axis(a0, a1): + if a0 is None or a1 is None: + raise Exception("Two axes need to be defined") + if not is_orthogonal(a0, a1): + raise Exception("Defined axes are not orthogonal") + return np.cross(a0, a1) + + @property + def basis(self): + return self._basis + + @property + def origin(self): + return self._origin + + @staticmethod + def change_of_base_rotation(css_from, css_to): + return np.linalg.solve(css_from.basis, css_to.basis) + + @staticmethod + def change_of_base_translation(css_from, css_to): + return css_to.origin - css_from.origin def vector_to_vector_transformation(u, v): From 6b3b42cf94732f2f96e20ae69fd7fc8d876570b4 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Fri, 10 Jan 2020 11:49:29 +0100 Subject: [PATCH 052/177] Remove third dimension from profile rasterization --- mypackage/geometry.py | 14 ++++++-------- mypackage/point_cloud_generator.py | 2 +- tests/test_geometry.py | 7 ++++--- tests/test_point_cloud_generator.py | 1 - 4 files changed, 11 insertions(+), 13 deletions(-) diff --git a/mypackage/geometry.py b/mypackage/geometry.py index 75a853c..baedda3 100644 --- a/mypackage/geometry.py +++ b/mypackage/geometry.py @@ -143,9 +143,6 @@ def rasterize(raster_width, point_start, point_end): """ length = np.linalg.norm(point_end - point_start) - point_start = np.append(point_start, [0]) - point_end = np.append(point_end, [0]) - num_raster_segments = np.round(length / raster_width) nrw = 1. / num_raster_segments @@ -244,12 +241,13 @@ def _rasterize(self, vec_center_start, rotation_angles): :param rotation_angles: Array containing the rotation angles :return: Array of contour points (3d) """ - vec_center_start_3d = np.append(vec_center_start, - [0])[np.newaxis, :, np.newaxis] - point_center_3d = np.append(self._point_center, [0])[:, np.newaxis] + vec_center_start_3d = vec_center_start[np.newaxis, :, np.newaxis] + point_center_3d = self._point_center[:, np.newaxis] rotation_matrices = R.from_euler('z', rotation_angles).as_dcm() + rotation_matrices = rotation_matrices[:, 0:2, 0:2] + print(rotation_matrices) raster_data = np.matmul(rotation_matrices, vec_center_start_3d) + point_center_3d @@ -491,7 +489,7 @@ def rasterize(self, raster_width): points = self._points segments = self._segments - raster_data = np.empty([0, 3]) + raster_data = np.empty([0, 2]) for i in range(self.num_segments()): raster_data = np.vstack((raster_data, segments[i].rasterize(raster_width, @@ -499,5 +497,5 @@ def rasterize(self, raster_width): points[i + 1]))) raster_data = np.vstack( - (raster_data, np.append(self._points[-1], [0]))) + (raster_data, self._points[-1])) return raster_data diff --git a/mypackage/point_cloud_generator.py b/mypackage/point_cloud_generator.py index c6b5510..14b0aaa 100644 --- a/mypackage/point_cloud_generator.py +++ b/mypackage/point_cloud_generator.py @@ -49,7 +49,7 @@ def rasterize(self, raster_width): :param: raster_width: Raster width :return: Raster data """ - raster_data = np.empty([0, 3]) + raster_data = np.empty([0, 2]) for shape in self.shapes: raster_data = np.vstack( (raster_data, shape.rasterize(raster_width))) diff --git a/tests/test_geometry.py b/tests/test_geometry.py index af20750..5ffa0dd 100644 --- a/tests/test_geometry.py +++ b/tests/test_geometry.py @@ -131,13 +131,14 @@ def default_rasterization_tests(data, raster_width, point_start, point_end): # Check if first point of the data are identical to the segment start assert np.linalg.norm(data[0, 0:2] - point_start) < 1E-9 + point_dimension = data[0, :].size num_data_points = data[:, 0].size + + assert point_dimension == 2 + for i in range(num_data_points): point = data[i] - # Check that z-component is 0 - assert point[2] == 0. - # Check if the raster width is close to the specified value if i < num_data_points - 1: next_point = data[i + 1, 0:2] diff --git a/tests/test_point_cloud_generator.py b/tests/test_point_cloud_generator.py index 7cbdb8e..4a24b52 100644 --- a/tests/test_point_cloud_generator.py +++ b/tests/test_point_cloud_generator.py @@ -62,4 +62,3 @@ def test_profile_rasterization(): expected_raster_point_x = i * raster_width - 1 assert data[i, 0] - expected_raster_point_x < 1E-9 assert data[i, 1] == 0 - assert data[i, 2] == 0 From 302ffcd52f76efdd970de3a1cdcbb33a6e2ad278 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Tue, 14 Jan 2020 18:05:35 +0100 Subject: [PATCH 053/177] Finish draft version of point cloud generator --- mypackage/geometry.py | 9 +- mypackage/point_cloud_generator.py | 254 ++++++++++++++++++++++++----- 2 files changed, 223 insertions(+), 40 deletions(-) diff --git a/mypackage/geometry.py b/mypackage/geometry.py index baedda3..6a6828f 100644 --- a/mypackage/geometry.py +++ b/mypackage/geometry.py @@ -247,7 +247,6 @@ def _rasterize(self, vec_center_start, rotation_angles): rotation_matrices = R.from_euler('z', rotation_angles).as_dcm() rotation_matrices = rotation_matrices[:, 0:2, 0:2] - print(rotation_matrices) raster_data = np.matmul(rotation_matrices, vec_center_start_3d) + point_center_3d @@ -475,6 +474,14 @@ def num_points(self): """ return self._points[:, 0].size + @property + def segments(self): + return self._segments + + @property + def points(self): + return self._points + def rasterize(self, raster_width): """ Create an array of points that describe the shapes contour. diff --git a/mypackage/point_cloud_generator.py b/mypackage/point_cloud_generator.py index 14b0aaa..f70e5d7 100644 --- a/mypackage/point_cloud_generator.py +++ b/mypackage/point_cloud_generator.py @@ -2,6 +2,7 @@ import mypackage.geometry as geo import numpy as np +import copy from scipy.spatial.transform import Rotation as R @@ -14,7 +15,7 @@ def __init__(self, shapes): :param: shapes: Instance or list of geo.Shape2D class(es) """ - self.shapes = [] + self._shapes = [] self.add_shapes(shapes) @@ -32,7 +33,7 @@ def add_shapes(self, shapes): raise TypeError( "Only instances or lists of Shape2d objects are accepted.") - self.shapes += shapes + self._shapes += shapes def num_shapes(self): """ @@ -40,7 +41,7 @@ def num_shapes(self): :return: Number of shapes """ - return len(self.shapes) + return len(self._shapes) def rasterize(self, raster_width): """ @@ -50,14 +51,28 @@ def rasterize(self, raster_width): :return: Raster data """ raster_data = np.empty([0, 2]) - for shape in self.shapes: + for shape in self._shapes: raster_data = np.vstack( (raster_data, shape.rasterize(raster_width))) return raster_data + @property + def shapes(self): + return self._shapes + -def is_point_valid(point): +def check_is_point2d(point): + if not isinstance(point, np.ndarray): + raise TypeError("Point data must be a numpy array") + if not len(point.shape) == 1: + raise ValueError("Point must be a 1d array") + if not point.size == 2: + raise ValueError("Point must have a size of 2") + return True + + +def is_point3d(point): if not isinstance(point, np.ndarray): return False if not point.size == 3: @@ -70,23 +85,42 @@ def is_orthogonal(u, v, tolerance=1E-9): class CartesianCoordinateSystem3d: - def __init__(self, x=None, y=None, z=None, origin=[0, 0, 0], + def __init__(self, basis=None, x=None, y=None, z=None, + origin=np.array([0, 0, 0]), positive_orientation=True): if positive_orientation: sign_orientation = 1 else: sign_orientation = -1 + if basis is not None: + x = basis[0] + y = basis[1] + z = basis[2] + + if x is None and y is None and z is None: + e0 = np.array([1, 0, 0]) + e1 = np.array([0, 1, 0]) + e2 = np.array([0, 0, 1]) * sign_orientation + elif x is not None and y is not None and z is not None: + if not (is_orthogonal(x, y) and + is_orthogonal(y, z) and + is_orthogonal(z, x)): + raise Exception("Basis vectors must be orthogonal") + e0 = x / np.linalg.norm(x) + e1 = y / np.linalg.norm(y) + e2 = z / np.linalg.norm(z) + else: + if x is None: + x = self._calcualte_third_axis(y, z) * sign_orientation + if y is None: + y = self._calcualte_third_axis(z, x) * sign_orientation + if z is None: + z = self._calcualte_third_axis(x, y) * sign_orientation - if x is None: - x = self._calcualte_third_axis(y, z) * sign_orientation - if y is None: - y = self._calcualte_third_axis(z, x) * sign_orientation - if z is None: - z = self._calcualte_third_axis(x, y) * sign_orientation + e0 = x / np.linalg.norm(x) + e1 = y / np.linalg.norm(y) + e2 = z / np.linalg.norm(z) - e0 = x / np.linalg.norm(x) - e1 = y / np.linalg.norm(y) - e2 = z / np.linalg.norm(z) self._basis = [e0, e1, e2] self._origin = np.array(origin) @@ -112,7 +146,7 @@ def change_of_base_rotation(css_from, css_to): @staticmethod def change_of_base_translation(css_from, css_to): - return css_to.origin - css_from.origin + return css_from.origin - css_to.origin def vector_to_vector_transformation(u, v): @@ -124,53 +158,195 @@ def vector_to_vector_transformation(u, v): return R.from_quat(unit_quaternion).as_dcm() -class LinearTrace: +class LinearHorizontalTraceSegment: def __init__(self, point_start, point_end): point_start = np.array(point_start) point_end = np.array(point_end) - if not is_point_valid(point_start): - raise TypeError("point_start is not a valid 3d point.") - if not is_point_valid(point_end): - raise TypeError("point_end is not a valid 3d point.") + check_is_point2d(point_start) + check_is_point2d(point_end) self._points = np.array([point_start, point_end]) self._direction_vector = self._points[1] - self._points[0] + self._length = np.linalg.norm(self._direction_vector) - def position(self, weight): + def _position(self, weight): weight = np.clip(weight, 0, 1) - return self._points[0] + weight * self._direction_vector + current_position = self._points[0] + weight * self._direction_vector + return np.append(current_position, [0]) + + def length(self): + return self._length + + def local_coordinate_system(self, weight): + weight = np.clip(weight, 0, 1) + direction = np.append(self._direction_vector, [0]) + return CartesianCoordinateSystem3d(y=direction, z=[0, 0, 1], + origin=self._position(weight)) + + +class Trace: + def __init__(self, + segments, + coordinate_system=CartesianCoordinateSystem3d()): + self._segments = segments + self._coordinate_system = coordinate_system + length = 0 + for segment in self._segments: + length += segment.length() + self._length = length + + def length(self): + return self._length + + def local_coordinate_system(self, position): + position = np.clip(position, 0, self._length) + cs_base = CartesianCoordinateSystem3d() + cs_stack = copy.deepcopy(self._coordinate_system) + + for i in range(len(self._segments)): + segment_length = self._segments[i].length() + if position <= segment_length: + weight = position / segment_length + else: + weight = 1 + + cs_local = self._segments[i].local_coordinate_system(weight) + tra = CartesianCoordinateSystem3d.change_of_base_translation( + cs_local, cs_base) + rot = CartesianCoordinateSystem3d.change_of_base_rotation( + cs_base, cs_local) + rot_tra = CartesianCoordinateSystem3d.change_of_base_rotation( + cs_stack, cs_base) + + tra = np.matmul(rot_tra, tra) + + basis = np.matmul(rot, cs_stack.basis) + origin = cs_stack.origin + tra + cs_stack = CartesianCoordinateSystem3d(basis=basis, + origin=origin) + + if position <= segment_length: + return cs_stack + position -= segment_length + + +class ProfileInterpolationLSBS: + """Linear segment by segment profile interpolation class.""" + + @staticmethod + def interpolate(a, b, weight): + weight = np.clip(weight, 0, 1) + if not len(a.shapes) == len(b.shapes): + raise Exception("Number of profile shapes do not match.") + + shapes = [] + for i in range(len(a.shapes)): + points_a = a.shapes[i].points + points_b = b.shapes[i].points - def direction(self, weight): - return self._direction_vector + if points_a[:, 0].size != points_b[:, 0].size: + raise Exception("Number of shape segments do not match.") + + points = (1 - weight) * points_a + weight * points_b + + segments_a = a.shapes[i].segments + segments_b = b.shapes[i].segments + + for j in range(len(segments_a)): + if not isinstance(segments_a[j], geo.Shape2D.LineSegment): + raise Exception( + "Only line segments are currently supported") + + if not isinstance(segments_a[j], type(segments_b[j])): + raise Exception("Shape segment types do not match.") + + if j == 0: + shapes += [geo.Shape2D(points[j], points[j + 1])] + else: + shapes[i].add_segment(points[j + 1]) + return Profile(shapes) + + +def to_list(var): + if isinstance(var, list): + return var + if var is None: + return [] + return [var] class Section: """Defines a section""" - def __init__(self, profile_start, profile_end, trace): + def __init__(self, profiles, trace, profile_interpolations=None, + profile_positions=None): - if not isinstance(profile_start, Profile): - raise TypeError("profile_start must be a Profile object.") - if not isinstance(profile_end, Profile): - raise TypeError("profile_end must be a Profile object.") - self._profiles = [profile_start, profile_end] + profiles = to_list(profiles) + profile_positions = to_list(profile_positions) + profile_interpolations = to_list(profile_interpolations) + + if not all(isinstance(profile, Profile) for profile in profiles): + raise TypeError( + "Only instances of lists or Shape2d objects are accepted.") + + if len(profiles) > 1 and len(profile_interpolations) != len( + profiles) - 1: + raise Exception( + "Number of interpolations must be one less than number " + "of profiles") + + if len(profiles) > 2 and len(profile_positions) != len(profiles) - 2: + raise Exception( + "If more than two profiles are used, the positions of the " + "profiles between the first and last one need to be " + "specified in a list of size: num_profiles -2.") + + self._profiles = profiles + self._interpolations = profile_interpolations + self._positions = [0] + profile_positions + [trace.length()] self.trace = trace - def rasterize(self): + def _profile_segment_index(self, position): + position = np.clip(position, 0, self._positions[-1]) + idx = 0 + while position > self._positions[idx + 1]: + idx += 1 + return idx + + def _interpolated_profile(self, position): + if len(self._profiles) == 1: + return self._profiles[0] + else: + idx = self._profile_segment_index(position) + weight = (position - self._positions[idx]) / ( + self._positions[idx + 1] - self._positions[idx]) + profile = self._interpolations[idx].interpolate( + self._profiles[idx], self._profiles[idx + 1], weight) + + return profile + + def rasterize(self, raster_width): raster_data = np.empty([0, 3]) trace = self.trace - for weight in np.arange(0, 1.01, 0.1): - profile = self._profiles[0] - profile_raster_data = profile.rasterize(0.1) + global_cs = CartesianCoordinateSystem3d() + for position in np.arange(0, trace.length() + 0.01, 0.5): + profile = self._interpolated_profile(position) + profile_raster_data = profile.rasterize(raster_width) + profile_raster_data = np.insert(profile_raster_data, 1, 0, axis=1) + trace_cs = trace.local_coordinate_system(position) + + rotation = CartesianCoordinateSystem3d.change_of_base_rotation( + trace_cs, global_cs) + translation = \ + CartesianCoordinateSystem3d.change_of_base_translation( + trace_cs, global_cs) - matrix = vector_to_vector_transformation([0, 0, -1], - trace.direction(weight)) profile_raster_data = np.matmul(profile_raster_data, - np.transpose(matrix)) - profile_raster_data += trace.position(weight) + np.transpose(rotation)) + profile_raster_data += translation raster_data = np.vstack((raster_data, profile_raster_data)) From 3ba6b174c2ea8ba8eb0ac1102ca98c13fed09d5e Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Wed, 15 Jan 2020 17:00:03 +0100 Subject: [PATCH 054/177] Add test for cartesian coordinate system --- mypackage/point_cloud_generator.py | 113 +++------------ mypackage/transformations.py | 206 ++++++++++++++++++++++++++++ tests/test_point_cloud_generator.py | 4 + tests/test_trasformations.py | 89 ++++++++++++ 4 files changed, 315 insertions(+), 97 deletions(-) create mode 100644 mypackage/transformations.py create mode 100644 tests/test_trasformations.py diff --git a/mypackage/point_cloud_generator.py b/mypackage/point_cloud_generator.py index f70e5d7..aff9076 100644 --- a/mypackage/point_cloud_generator.py +++ b/mypackage/point_cloud_generator.py @@ -3,6 +3,7 @@ import mypackage.geometry as geo import numpy as np import copy +import mypackage.transformations as tf from scipy.spatial.transform import Rotation as R @@ -16,7 +17,6 @@ def __init__(self, shapes): :param: shapes: Instance or list of geo.Shape2D class(es) """ self._shapes = [] - self.add_shapes(shapes) def add_shapes(self, shapes): @@ -80,84 +80,6 @@ def is_point3d(point): return True -def is_orthogonal(u, v, tolerance=1E-9): - return np.abs(np.dot(u, v)) < tolerance - - -class CartesianCoordinateSystem3d: - def __init__(self, basis=None, x=None, y=None, z=None, - origin=np.array([0, 0, 0]), - positive_orientation=True): - if positive_orientation: - sign_orientation = 1 - else: - sign_orientation = -1 - if basis is not None: - x = basis[0] - y = basis[1] - z = basis[2] - - if x is None and y is None and z is None: - e0 = np.array([1, 0, 0]) - e1 = np.array([0, 1, 0]) - e2 = np.array([0, 0, 1]) * sign_orientation - elif x is not None and y is not None and z is not None: - if not (is_orthogonal(x, y) and - is_orthogonal(y, z) and - is_orthogonal(z, x)): - raise Exception("Basis vectors must be orthogonal") - e0 = x / np.linalg.norm(x) - e1 = y / np.linalg.norm(y) - e2 = z / np.linalg.norm(z) - else: - if x is None: - x = self._calcualte_third_axis(y, z) * sign_orientation - if y is None: - y = self._calcualte_third_axis(z, x) * sign_orientation - if z is None: - z = self._calcualte_third_axis(x, y) * sign_orientation - - e0 = x / np.linalg.norm(x) - e1 = y / np.linalg.norm(y) - e2 = z / np.linalg.norm(z) - - self._basis = [e0, e1, e2] - self._origin = np.array(origin) - - @staticmethod - def _calcualte_third_axis(a0, a1): - if a0 is None or a1 is None: - raise Exception("Two axes need to be defined") - if not is_orthogonal(a0, a1): - raise Exception("Defined axes are not orthogonal") - return np.cross(a0, a1) - - @property - def basis(self): - return self._basis - - @property - def origin(self): - return self._origin - - @staticmethod - def change_of_base_rotation(css_from, css_to): - return np.linalg.solve(css_from.basis, css_to.basis) - - @staticmethod - def change_of_base_translation(css_from, css_to): - return css_from.origin - css_to.origin - - -def vector_to_vector_transformation(u, v): - r = np.cross(u, v) - w = np.sqrt(np.dot(u, u) * np.dot(v, v)) + np.dot(u, v) - quaternion = np.concatenate((r, [w])) - unit_quaternion = quaternion / np.linalg.norm(quaternion) - - return R.from_quat(unit_quaternion).as_dcm() - - class LinearHorizontalTraceSegment: def __init__(self, point_start, point_end): @@ -180,16 +102,19 @@ def length(self): return self._length def local_coordinate_system(self, weight): + ccs = tf.CartesianCoordinateSystem3d weight = np.clip(weight, 0, 1) direction = np.append(self._direction_vector, [0]) - return CartesianCoordinateSystem3d(y=direction, z=[0, 0, 1], - origin=self._position(weight)) + return ccs.construct_from_yz_and_orientation(direction, + [0, 0, 1], + True, + self._position(weight)) class Trace: def __init__(self, segments, - coordinate_system=CartesianCoordinateSystem3d()): + coordinate_system=tf.CartesianCoordinateSystem3d()): self._segments = segments self._coordinate_system = coordinate_system length = 0 @@ -202,7 +127,7 @@ def length(self): def local_coordinate_system(self, position): position = np.clip(position, 0, self._length) - cs_base = CartesianCoordinateSystem3d() + cs_base = tf.CartesianCoordinateSystem3d() cs_stack = copy.deepcopy(self._coordinate_system) for i in range(len(self._segments)): @@ -213,19 +138,16 @@ def local_coordinate_system(self, position): weight = 1 cs_local = self._segments[i].local_coordinate_system(weight) - tra = CartesianCoordinateSystem3d.change_of_base_translation( - cs_local, cs_base) - rot = CartesianCoordinateSystem3d.change_of_base_rotation( - cs_base, cs_local) - rot_tra = CartesianCoordinateSystem3d.change_of_base_rotation( - cs_stack, cs_base) + tra = tf.change_of_base_translation(cs_local, cs_base) + rot = tf.change_of_base_rotation(cs_base, cs_local) + rot_tra = tf.change_of_base_rotation(cs_stack, cs_base) tra = np.matmul(rot_tra, tra) basis = np.matmul(rot, cs_stack.basis) origin = cs_stack.origin + tra - cs_stack = CartesianCoordinateSystem3d(basis=basis, - origin=origin) + cs_stack = tf.CartesianCoordinateSystem3d(basis=basis, + origin=origin) if position <= segment_length: return cs_stack @@ -331,18 +253,15 @@ def rasterize(self, raster_width): raster_data = np.empty([0, 3]) trace = self.trace - global_cs = CartesianCoordinateSystem3d() + global_cs = tf.CartesianCoordinateSystem3d() for position in np.arange(0, trace.length() + 0.01, 0.5): profile = self._interpolated_profile(position) profile_raster_data = profile.rasterize(raster_width) profile_raster_data = np.insert(profile_raster_data, 1, 0, axis=1) trace_cs = trace.local_coordinate_system(position) - rotation = CartesianCoordinateSystem3d.change_of_base_rotation( - trace_cs, global_cs) - translation = \ - CartesianCoordinateSystem3d.change_of_base_translation( - trace_cs, global_cs) + rotation = tf.change_of_base_rotation(trace_cs, global_cs) + translation = tf.change_of_base_translation(trace_cs, global_cs) profile_raster_data = np.matmul(profile_raster_data, np.transpose(rotation)) diff --git a/mypackage/transformations.py b/mypackage/transformations.py new file mode 100644 index 0000000..c1d8c16 --- /dev/null +++ b/mypackage/transformations.py @@ -0,0 +1,206 @@ +import numpy as np +from scipy.spatial.transform import Rotation as Rot + + +def rotation_matrix_x(angle): + return Rot.from_euler("x", angle).as_dcm() + + +def rotation_matrix_y(angle): + return Rot.from_euler("y", angle).as_dcm() + + +def rotation_matrix_z(angle): + return Rot.from_euler("z", angle).as_dcm() + + +def normalize(u): + norm = np.linalg.norm(u) + if norm == 0.: + raise Exception("Vector length is 0.") + return u / norm + + +def point_left_of_plane_by_vectors(point, plane_vec0, plane_vec1): + # right is the direction of the cross product + print([plane_vec0, plane_vec1, point]) + print(np.sign(np.linalg.det([plane_vec0, plane_vec1, point]))) + return np.sign(np.linalg.det([plane_vec0, plane_vec1, point])) + + +def point_left_of_plane_by_points(point, plane_a, plane_b, plane_c): + vec_a_b = plane_b - plane_a + vec_a_c = plane_c - plane_a + vec_a_point = point - plane_a + return point_left_of_plane_by_vectors(vec_a_b, vec_a_c, vec_a_point) + + +def is_orthogonal(u, v, tolerance=1E-9): + return np.abs(np.dot(normalize(u), normalize(v))) < tolerance + + +def change_of_base_rotation(css_from, css_to): + return np.linalg.solve(css_from.basis, css_to.basis) + + +def change_of_base_translation(css_from, css_to): + return css_from.origin - css_to.origin + + +class CartesianCoordinateSystem3d: + """Defines a 3d cartesian coordinate system.""" + + def __init__(self, basis=np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]]), + origin=np.array([0, 0, 0])): + """ + Construct a cartesian coordinate system. + + :param basis: List of basis vectors + :param origin: Position of the origin + :return: Cartesian coordinate system + """ + basis[0] = normalize(basis[0]) + basis[1] = normalize(basis[1]) + basis[2] = normalize(basis[2]) + + if not (is_orthogonal(basis[0], basis[1]) and + is_orthogonal(basis[1], basis[2]) and + is_orthogonal(basis[2], basis[0])): + raise Exception("Basis vectors must be orthogonal") + + self._basis = basis + + self._origin = np.array(origin) + + @classmethod + def construct_from_basis(cls, basis, origin=np.array([0, 0, 0])): + """ + Construct a cartesian coordinate system. + + :param basis: List of basis vectors + :param origin: Position of the origin + :return: Cartesian coordinate system + """ + return cls(basis, origin=origin) + + @classmethod + def construct_from_xyz(cls, x, y, z, origin=np.array([0, 0, 0])): + """ + Construct a cartesian coordinate system. + + :param x: Vector defining the x-axis + :param y: Vector defining the y-axis + :param z: Vector defining the z-axis + :param origin: Position of the origin + :return: Cartesian coordinate system + """ + basis = [x, y, z] + return cls(basis, origin=origin) + + @classmethod + def construct_from_xy_and_orientation(cls, x, y, positive_orientation=True, + origin=np.array([0, 0, 0])): + """ + Construct a cartesian coordinate system. + + :param x: Vector defining the x-axis + :param y: Vector defining the y-axis + :param positive_orientation: Set to True if the orientation should + be positive and to False if not + :param origin: Position of the origin + :return: Cartesian coordinate system + """ + z = cls._calcualte_orthogonal_axis(x, y) * cls._sign_orientation( + positive_orientation) + basis = [x, y, z] + return cls(basis, origin=origin) + + @classmethod + def construct_from_yz_and_orientation(cls, y, z, positive_orientation=True, + origin=np.array([0, 0, 0])): + """ + Construct a cartesian coordinate system. + + :param y: Vector defining the y-axis + :param z: Vector defining the z-axis + :param positive_orientation: Set to True if the orientation should + be positive and to False if not + :param origin: Position of the origin + :return: Cartesian coordinate system + """ + x = cls._calcualte_orthogonal_axis(y, z) * cls._sign_orientation( + positive_orientation) + basis = [x, y, z] + return cls(basis, origin=origin) + + @classmethod + def construct_from_xz_and_orientation(cls, x, z, positive_orientation=True, + origin=np.array([0, 0, 0])): + """ + Construct a cartesian coordinate system. + + :param x: Vector defining the x-axis + :param z: Vector defining the z-axis + :param positive_orientation: Set to True if the orientation should + be positive and to False if not + :param origin: Position of the origin + :return: Cartesian coordinate system + """ + y = cls._calcualte_orthogonal_axis(z, x) * cls._sign_orientation( + positive_orientation) + basis = [x, y, z] + return cls(basis, origin=origin) + + @staticmethod + def _sign_orientation(positive_orientation): + """ + Get -1 or 1 depending on the coordinate systems orientation. + + :param positive_orientation: Set to True if the orientation should + be positive and to False if not + :return: 1 if the coordinate system has positive orientation, + -1 otherwise + """ + if positive_orientation: + return 1 + return -1 + + @staticmethod + def _calcualte_orthogonal_axis(a0, a1): + """ + Calculate an axis which is orthogonal to two other axes. + + The calculated axis has a positive orientation towards the other 2 + axes. + + :param a0: First axis + :param a1: Second axis + :return: Orthogonal axis + """ + return np.cross(a0, a1) + + @property + def basis(self): + """ + Get the coordinate systems basis. + + :return: Basis of the coordinate system + """ + return self._basis + + @property + def origin(self): + """ + Get the coordinate systems origin. + + :return: Origin of the coordinate system + """ + return self._origin + +# def vector_to_vector_transformation(u, v): +# r = np.cross(u, v) +# w = np.sqrt(np.dot(u, u) * np.dot(v, v)) + np.dot(u, v) +# quaternion = np.concatenate((r, [w])) +# unit_quaternion = quaternion / np.linalg.norm(quaternion) + +# return R.from_quat(unit_quaternion).as_dcm() diff --git a/tests/test_point_cloud_generator.py b/tests/test_point_cloud_generator.py index 4a24b52..e7c2c00 100644 --- a/tests/test_point_cloud_generator.py +++ b/tests/test_point_cloud_generator.py @@ -1,8 +1,12 @@ import pytest import mypackage.point_cloud_generator as pcg import mypackage.geometry as geo +import mypackage.transformations as tf +import numpy as np +# profile class --------------------------------------------------------------- + def test_profile_construction_and_shape_addition(): shape = geo.Shape2D([0, 0], [1, 0]) shape.add_segment([2, -1]) diff --git a/tests/test_trasformations.py b/tests/test_trasformations.py new file mode 100644 index 0000000..1d805ae --- /dev/null +++ b/tests/test_trasformations.py @@ -0,0 +1,89 @@ +import mypackage.transformations as tf +import numpy as np + + +# cartesian coordinate system class ------------------------------------------- + +def is_orientation_positive(ccs): + return tf.point_left_of_plane_by_vectors(ccs.basis[2], ccs.basis[0], + ccs.basis[1]) > 0 + + +def check_coordinate_system(ccs, basis_expected, origin_expected, + positive_orientation_expected): + # check orientation is as expected + assert is_orientation_positive(ccs) == positive_orientation_expected + + for i in range(3): + unit_vec = tf.normalize(basis_expected[i]) + + # check axis orientations match + if not np.abs(np.dot(ccs.basis[i], unit_vec) - 1) < 1E-9: + grr = 0 + assert np.abs(np.dot(ccs.basis[i], unit_vec) - 1) < 1E-9 + + # check origin correct + assert np.abs(origin_expected[i] - ccs.origin[i]) < 1E-9 + + +def test_cartesian_coordinate_system_construction(): + # alias name for class - name is too long :) + cls_ccs = tf.CartesianCoordinateSystem3d + + # setup ----------------------------------------------- + origin = [4, -2, 6] + x = [1, 0, 0] + y = [0, 1, 0] + z = [0, 0, 1] + + # rotate axes to produce a more general test case + angle_x = np.pi / 3 + angle_y = np.pi / 4 + angle_z = np.pi / 5 + r_x = tf.rotation_matrix_x(angle_x) + r_y = tf.rotation_matrix_y(angle_y) + r_z = tf.rotation_matrix_z(angle_z) + + r_tot = np.matmul(r_z, np.matmul(r_y, r_x)) + + x = np.matmul(r_tot, x) + y = np.matmul(r_tot, y) + z = np.matmul(r_tot, z) + + basis_pos = [x, y, z] + basis_neg = [x, y, -z] + + # construction with x,y,z-vectors --------------------- + + ccs_xyz_pos = cls_ccs.construct_from_xyz(x, y, z, origin) + ccs_xyz_neg = cls_ccs.construct_from_xyz(x, y, -z, origin) + + check_coordinate_system(ccs_xyz_pos, basis_pos, origin, True) + check_coordinate_system(ccs_xyz_neg, basis_neg, origin, False) + + # construction with x,y-vectors and orientation ------- + ccs_xyo_pos = cls_ccs.construct_from_xy_and_orientation(x, y, True, origin) + ccs_xyo_neg = cls_ccs.construct_from_xy_and_orientation(x, y, False, + origin) + + check_coordinate_system(ccs_xyo_pos, basis_pos, origin, True) + check_coordinate_system(ccs_xyo_neg, basis_neg, origin, False) + + # construction with y,z-vectors and orientation ------- + ccs_yzo_pos = cls_ccs.construct_from_yz_and_orientation(y, z, True, origin) + ccs_yzo_neg = cls_ccs.construct_from_yz_and_orientation(y, -z, False, + origin) + + check_coordinate_system(ccs_yzo_pos, basis_pos, origin, True) + check_coordinate_system(ccs_yzo_neg, basis_neg, origin, False) + + # construction with x,z-vectors and orientation ------- + ccs_xzo_pos = cls_ccs.construct_from_xz_and_orientation(x, z, True, origin) + ccs_xzo_neg = cls_ccs.construct_from_xz_and_orientation(x, -z, False, + origin) + + check_coordinate_system(ccs_xzo_pos, basis_pos, origin, True) + check_coordinate_system(ccs_xzo_neg, basis_neg, origin, False) + + +test_cartesian_coordinate_system_construction() From 66bd8596521992c3ac9dcbeb37b2b3624fd7c9f0 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Wed, 15 Jan 2020 17:19:16 +0100 Subject: [PATCH 055/177] Add missing test cases --- tests/test_trasformations.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/tests/test_trasformations.py b/tests/test_trasformations.py index 1d805ae..6682392 100644 --- a/tests/test_trasformations.py +++ b/tests/test_trasformations.py @@ -1,5 +1,6 @@ import mypackage.transformations as tf import numpy as np +import pytest # cartesian coordinate system class ------------------------------------------- @@ -14,12 +15,15 @@ def check_coordinate_system(ccs, basis_expected, origin_expected, # check orientation is as expected assert is_orientation_positive(ccs) == positive_orientation_expected + # check basis vectors are orthogonal + assert tf.is_orthogonal(ccs.basis[0], ccs.basis[1]) + assert tf.is_orthogonal(ccs.basis[1], ccs.basis[2]) + assert tf.is_orthogonal(ccs.basis[2], ccs.basis[0]) + for i in range(3): unit_vec = tf.normalize(basis_expected[i]) # check axis orientations match - if not np.abs(np.dot(ccs.basis[i], unit_vec) - 1) < 1E-9: - grr = 0 assert np.abs(np.dot(ccs.basis[i], unit_vec) - 1) < 1E-9 # check origin correct @@ -53,6 +57,14 @@ def test_cartesian_coordinate_system_construction(): basis_pos = [x, y, z] basis_neg = [x, y, -z] + # construction with basis ----------------------------- + + ccs_basis_pos = cls_ccs.construct_from_basis(basis_pos, origin) + ccs_basis_neg = cls_ccs.construct_from_basis(basis_neg, origin) + + check_coordinate_system(ccs_basis_pos, basis_pos, origin, True) + check_coordinate_system(ccs_basis_neg, basis_neg, origin, False) + # construction with x,y,z-vectors --------------------- ccs_xyz_pos = cls_ccs.construct_from_xyz(x, y, z, origin) @@ -85,5 +97,6 @@ def test_cartesian_coordinate_system_construction(): check_coordinate_system(ccs_xzo_pos, basis_pos, origin, True) check_coordinate_system(ccs_xzo_neg, basis_neg, origin, False) - -test_cartesian_coordinate_system_construction() + # check exceptions ------------------------------------ + with pytest.raises(Exception): + cls_ccs([x, y, [0, 0, 1]]) From 11c31abbf2be8074bbd5ad4b5b4207ffb9f3fb3c Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Wed, 15 Jan 2020 18:03:07 +0100 Subject: [PATCH 056/177] Test single axis rotation matrices --- tests/test_trasformations.py | 48 ++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/tests/test_trasformations.py b/tests/test_trasformations.py index 6682392..cec8c71 100644 --- a/tests/test_trasformations.py +++ b/tests/test_trasformations.py @@ -3,6 +3,54 @@ import pytest +# functions ------------------------------------------------------------------- +def check_matrix_orthogonal(matrix): + transposed = np.transpose(matrix) + + product = np.matmul(transposed, matrix) + unit = np.identity(3) + for i in range(3): + for j in range(3): + assert np.abs(product[i][j] - unit[i][j]) < 1E-9 + + +def check_matrix_does_not_reflect(matrix): + assert np.linalg.det(matrix) >= 0 + + +def test_single_axis_rotation_matrices(): + matrix_funcs = [tf.rotation_matrix_x, tf.rotation_matrix_y, + tf.rotation_matrix_z] + vec = np.array([1, 1, 1]) + + for i in range(3): + for j in range(36): + angle = j / 18 * np.pi + matrix = matrix_funcs[i](angle) + + # rotation matrices are orthogonal + check_matrix_orthogonal(matrix) + + # matrix should not reflect + check_matrix_does_not_reflect(matrix) + + # rotate vector + res = np.matmul(matrix, vec) + + # check component of rotation axis + assert np.abs(res[i] - 1) < 1E-9 + + # check other components + i_1 = (i + 1) % 3 + i_2 = (i + 2) % 3 + + exp_1 = np.cos(angle) - np.sin(angle) + exp_2 = np.cos(angle) + np.sin(angle) + + assert np.abs(res[i_1] - exp_1) < 1E-9 + assert np.abs(res[i_2] - exp_2) < 1E-9 + + # cartesian coordinate system class ------------------------------------------- def is_orientation_positive(ccs): From 3cc6a0e133213a34d09594e8d6f57ca73d359898 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Thu, 16 Jan 2020 08:51:21 +0100 Subject: [PATCH 057/177] Add doc strings --- mypackage/transformations.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/mypackage/transformations.py b/mypackage/transformations.py index c1d8c16..ad9297e 100644 --- a/mypackage/transformations.py +++ b/mypackage/transformations.py @@ -3,14 +3,32 @@ def rotation_matrix_x(angle): + """ + Create a rotation matrix that rotates around the x-axis. + + :param angle: Rotation angle + :return: Rotation matrix + """ return Rot.from_euler("x", angle).as_dcm() def rotation_matrix_y(angle): + """ + Create a rotation matrix that rotates around the y-axis. + + :param angle: Rotation angle + :return: Rotation matrix + """ return Rot.from_euler("y", angle).as_dcm() def rotation_matrix_z(angle): + """ + Create a rotation matrix that rotates around the z-axis. + + :param angle: Rotation angle + :return: Rotation matrix + """ return Rot.from_euler("z", angle).as_dcm() From d5f5105e7a73f02887b45004861cb7345d3ca6bf Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Thu, 16 Jan 2020 09:31:59 +0100 Subject: [PATCH 058/177] Test normalize function --- mypackage/transformations.py | 6 ++++++ tests/test_trasformations.py | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/mypackage/transformations.py b/mypackage/transformations.py index ad9297e..9ff689f 100644 --- a/mypackage/transformations.py +++ b/mypackage/transformations.py @@ -33,6 +33,12 @@ def rotation_matrix_z(angle): def normalize(u): + """ + Normalize a vector. + + :param u: Vector + :return: Normalized vector + """ norm = np.linalg.norm(u) if norm == 0.: raise Exception("Vector length is 0.") diff --git a/tests/test_trasformations.py b/tests/test_trasformations.py index cec8c71..c3fa46a 100644 --- a/tests/test_trasformations.py +++ b/tests/test_trasformations.py @@ -1,6 +1,8 @@ import mypackage.transformations as tf import numpy as np import pytest +import random +import math # functions ------------------------------------------------------------------- @@ -51,6 +53,39 @@ def test_single_axis_rotation_matrices(): assert np.abs(res[i_2] - exp_2) < 1E-9 +def random_non_unit_vector(): + vec = np.array([random.random(), random.random(), + random.random()]) * 10 * random.random() + while math.isclose(np.linalg.norm(vec), 1) or math.isclose( + np.linalg.norm(vec), 0): + vec = np.array([random.random(), random.random(), + random.random()]) * 10 * random.random() + return vec + + +def test_normalize(): + for i in range(20): + vec = random_non_unit_vector() + + unit = tf.normalize(vec) + + # check that vector is modified + for i in range(vec.size): + assert not math.isclose(unit[i], vec[i]) + + # check length is 1 + assert math.isclose(np.linalg.norm(unit), 1) + + # check that both vectors point into the same direction + vec2 = unit * np.linalg.norm(vec) + for i in range(vec.size): + assert math.isclose(vec2[i], vec[i]) + + # check exception if length is 0 + with pytest.raises(Exception): + tf.normalize(np.array([0, 0, 0])) + + # cartesian coordinate system class ------------------------------------------- def is_orientation_positive(ccs): From cb395839c5d7e7b29fcf1b5a7631b66f39ec8c01 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Thu, 16 Jan 2020 11:22:28 +0100 Subject: [PATCH 059/177] Test orientation function (plane through origin) --- mypackage/transformations.py | 64 ++++++++++++++--- tests/test_trasformations.py | 136 ++++++++++++++++++++++------------- 2 files changed, 141 insertions(+), 59 deletions(-) diff --git a/mypackage/transformations.py b/mypackage/transformations.py index 9ff689f..a7f6296 100644 --- a/mypackage/transformations.py +++ b/mypackage/transformations.py @@ -1,4 +1,5 @@ import numpy as np +import math from scipy.spatial.transform import Rotation as Rot @@ -45,18 +46,61 @@ def normalize(u): return u / norm -def point_left_of_plane_by_vectors(point, plane_vec0, plane_vec1): - # right is the direction of the cross product - print([plane_vec0, plane_vec1, point]) - print(np.sign(np.linalg.det([plane_vec0, plane_vec1, point]))) - return np.sign(np.linalg.det([plane_vec0, plane_vec1, point])) +def orientation_point_plane_containing_origin(point, a, b): + """ + Determine a points orientation relative to a plane containing the origin. + + The side is defined by the winding order of the triangle 'origin - A - + B'. When looking at it from the left-hand side, the ordering is clockwise + and counter-clockwise when looking from the right-hand side. + + The function returns 1 if the point lies left of the plane, -1 if it is + on the right and 0 if it lies on the plane. + + Note, that this function is not appropriate to check if a point lies on + a plane since it has no tolerance to compensate for numerical errors. + + Additional note: The points A and B can also been considered as two + vectors spanning the plane. + + :param point: Point + :param a: Second point of the triangle 'origin - A - B'. + :param b: Third point of the triangle 'origin - A - B'. + :return: 1, -1 or 0 (see description) + """ + if (math.isclose(np.linalg.norm(a), 0) or + math.isclose(np.linalg.norm(b), 0) or + math.isclose(np.linalg.norm(b - a), 0)): + raise Exception( + "One or more points describing the plane are identical.") + return np.sign(np.linalg.det([a, b, point])) -def point_left_of_plane_by_points(point, plane_a, plane_b, plane_c): - vec_a_b = plane_b - plane_a - vec_a_c = plane_c - plane_a - vec_a_point = point - plane_a - return point_left_of_plane_by_vectors(vec_a_b, vec_a_c, vec_a_point) + +def point_left_of_plane_by_points(point, a, b, c): + """ + Determine a points orientation relative to a plane. + + The side is defined by the winding order of the triangle 'A -B - C'. + When looking at it from the left-hand side, the ordering is clockwise + and counter-clockwise when looking from the right-hand side. + + The function returns 1 if the point lies left of the plane, -1 if it is + on the right and 0 if it lies on the plane. + + Note, that this function is not appropriate to check if a point lies on + a plane since it has no tolerance to compensate for numerical errors. + + :param a: First point of the triangle 'A - B - C'. + :param b: Second point of the triangle 'A - B - C'. + :param b: Third point of the triangle 'A - B - C'. + :return: 1, -1 or 0 (see description) + """ + vec_a_b = b - a + vec_a_c = c - a + vec_a_point = point - a + return orientation_point_plane_containing_origin(vec_a_b, vec_a_c, + vec_a_point) def is_orthogonal(u, v, tolerance=1E-9): diff --git a/tests/test_trasformations.py b/tests/test_trasformations.py index c3fa46a..f60e9bf 100644 --- a/tests/test_trasformations.py +++ b/tests/test_trasformations.py @@ -5,7 +5,32 @@ import math -# functions ------------------------------------------------------------------- +# helpers for tests ----------------------------------------------------------- + +def check_coordinate_system(ccs, basis_expected, origin_expected, + positive_orientation_expected): + # check orientation is as expected + assert is_orientation_positive(ccs) == positive_orientation_expected + + # check basis vectors are orthogonal + assert tf.is_orthogonal(ccs.basis[0], ccs.basis[1]) + assert tf.is_orthogonal(ccs.basis[1], ccs.basis[2]) + assert tf.is_orthogonal(ccs.basis[2], ccs.basis[0]) + + for i in range(3): + unit_vec = tf.normalize(basis_expected[i]) + + # check axis orientations match + assert np.abs(np.dot(ccs.basis[i], unit_vec) - 1) < 1E-9 + + # check origin correct + assert np.abs(origin_expected[i] - ccs.origin[i]) < 1E-9 + + +def check_matrix_does_not_reflect(matrix): + assert np.linalg.det(matrix) >= 0 + + def check_matrix_orthogonal(matrix): transposed = np.transpose(matrix) @@ -16,9 +41,45 @@ def check_matrix_orthogonal(matrix): assert np.abs(product[i][j] - unit[i][j]) < 1E-9 -def check_matrix_does_not_reflect(matrix): - assert np.linalg.det(matrix) >= 0 +def is_orientation_positive(ccs): + return tf.orientation_point_plane_containing_origin(ccs.basis[2], + ccs.basis[0], + ccs.basis[1]) > 0 + + +def random_non_unit_vector(): + vec = np.array([random.random(), random.random(), + random.random()]) * 10 * random.random() + while math.isclose(np.linalg.norm(vec), 1) or math.isclose( + np.linalg.norm(vec), 0): + vec = np.array([random.random(), random.random(), + random.random()]) * 10 * random.random() + return vec + + +def rotated_positive_orthogonal_base(): + x = [1, 0, 0] + y = [0, 1, 0] + z = [0, 0, 1] + # rotate axes to produce a more general test case + angle_x = np.pi / 3 + angle_y = np.pi / 4 + angle_z = np.pi / 5 + r_x = tf.rotation_matrix_x(angle_x) + r_y = tf.rotation_matrix_y(angle_y) + r_z = tf.rotation_matrix_z(angle_z) + + r_tot = np.matmul(r_z, np.matmul(r_y, r_x)) + + x = np.matmul(r_tot, x) + y = np.matmul(r_tot, y) + z = np.matmul(r_tot, z) + + return [x, y, z] + + +# test functions -------------------------------------------------------------- def test_single_axis_rotation_matrices(): matrix_funcs = [tf.rotation_matrix_x, tf.rotation_matrix_y, @@ -53,16 +114,6 @@ def test_single_axis_rotation_matrices(): assert np.abs(res[i_2] - exp_2) < 1E-9 -def random_non_unit_vector(): - vec = np.array([random.random(), random.random(), - random.random()]) * 10 * random.random() - while math.isclose(np.linalg.norm(vec), 1) or math.isclose( - np.linalg.norm(vec), 0): - vec = np.array([random.random(), random.random(), - random.random()]) * 10 * random.random() - return vec - - def test_normalize(): for i in range(20): vec = random_non_unit_vector() @@ -86,32 +137,35 @@ def test_normalize(): tf.normalize(np.array([0, 0, 0])) -# cartesian coordinate system class ------------------------------------------- +def test_orientation_point_plane_containing_origin(): + [a, b, n] = rotated_positive_orthogonal_base() + a *= 2.3 + b /= 1.5 -def is_orientation_positive(ccs): - return tf.point_left_of_plane_by_vectors(ccs.basis[2], ccs.basis[0], - ccs.basis[1]) > 0 + for length in np.arange(-9.5, 9.51, 1): + orientation = tf.orientation_point_plane_containing_origin(n * length, + a, b) + assert np.sign(length) == orientation + # check exceptions + with pytest.raises(Exception): + tf.orientation_point_plane_containing_origin(n, a, a) + with pytest.raises(Exception): + tf.orientation_point_plane_containing_origin(n, np.zeros(3), b) + with pytest.raises(Exception): + tf.orientation_point_plane_containing_origin(n, a, np.zeros(3)) -def check_coordinate_system(ccs, basis_expected, origin_expected, - positive_orientation_expected): - # check orientation is as expected - assert is_orientation_positive(ccs) == positive_orientation_expected - - # check basis vectors are orthogonal - assert tf.is_orthogonal(ccs.basis[0], ccs.basis[1]) - assert tf.is_orthogonal(ccs.basis[1], ccs.basis[2]) - assert tf.is_orthogonal(ccs.basis[2], ccs.basis[0]) + # check special case point on plane + a = np.array([1, 0, 0]) + b = np.array([0, 1, 0]) + orientation = tf.orientation_point_plane_containing_origin(a, a, b) + assert orientation == 0 - for i in range(3): - unit_vec = tf.normalize(basis_expected[i]) - # check axis orientations match - assert np.abs(np.dot(ccs.basis[i], unit_vec) - 1) < 1E-9 +test_orientation_point_plane_containing_origin() - # check origin correct - assert np.abs(origin_expected[i] - ccs.origin[i]) < 1E-9 +# test cartesian coordinate system class -------------------------------------- def test_cartesian_coordinate_system_construction(): # alias name for class - name is too long :) @@ -119,23 +173,7 @@ def test_cartesian_coordinate_system_construction(): # setup ----------------------------------------------- origin = [4, -2, 6] - x = [1, 0, 0] - y = [0, 1, 0] - z = [0, 0, 1] - - # rotate axes to produce a more general test case - angle_x = np.pi / 3 - angle_y = np.pi / 4 - angle_z = np.pi / 5 - r_x = tf.rotation_matrix_x(angle_x) - r_y = tf.rotation_matrix_y(angle_y) - r_z = tf.rotation_matrix_z(angle_z) - - r_tot = np.matmul(r_z, np.matmul(r_y, r_x)) - - x = np.matmul(r_tot, x) - y = np.matmul(r_tot, y) - z = np.matmul(r_tot, z) + [x, y, z] = rotated_positive_orthogonal_base() basis_pos = [x, y, z] basis_neg = [x, y, -z] From 734bf80b8f23ed8d4ed51a1c2068bd4a8f901404 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Thu, 16 Jan 2020 11:40:45 +0100 Subject: [PATCH 060/177] Test orientation function (plane defined by points) --- mypackage/transformations.py | 13 +++++++------ tests/test_trasformations.py | 27 ++++++++++++++++++++++++++- 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/mypackage/transformations.py b/mypackage/transformations.py index a7f6296..c9ec992 100644 --- a/mypackage/transformations.py +++ b/mypackage/transformations.py @@ -77,11 +77,11 @@ def orientation_point_plane_containing_origin(point, a, b): return np.sign(np.linalg.det([a, b, point])) -def point_left_of_plane_by_points(point, a, b, c): +def orientation_point_plane(point, a, b, c): """ - Determine a points orientation relative to a plane. + Determine a points orientation relative to an arbitrary plane. - The side is defined by the winding order of the triangle 'A -B - C'. + The side is defined by the winding order of the triangle 'A - B - C'. When looking at it from the left-hand side, the ordering is clockwise and counter-clockwise when looking from the right-hand side. @@ -91,16 +91,17 @@ def point_left_of_plane_by_points(point, a, b, c): Note, that this function is not appropriate to check if a point lies on a plane since it has no tolerance to compensate for numerical errors. + :param point: Point :param a: First point of the triangle 'A - B - C'. :param b: Second point of the triangle 'A - B - C'. - :param b: Third point of the triangle 'A - B - C'. + :param c: Third point of the triangle 'A - B - C'. :return: 1, -1 or 0 (see description) """ vec_a_b = b - a vec_a_c = c - a vec_a_point = point - a - return orientation_point_plane_containing_origin(vec_a_b, vec_a_c, - vec_a_point) + return orientation_point_plane_containing_origin(vec_a_point, vec_a_b, + vec_a_c) def is_orthogonal(u, v, tolerance=1E-9): diff --git a/tests/test_trasformations.py b/tests/test_trasformations.py index f60e9bf..708fe2f 100644 --- a/tests/test_trasformations.py +++ b/tests/test_trasformations.py @@ -162,7 +162,32 @@ def test_orientation_point_plane_containing_origin(): assert orientation == 0 -test_orientation_point_plane_containing_origin() +def test_orientation_point_plane(): + [b, c, n] = rotated_positive_orthogonal_base() + a = [3.2, -2.1, 5.4] + b = b * 6.5 + a + c = c * 0.3 + a + + for length in np.arange(-9.5, 9.51, 1): + orientation = tf.orientation_point_plane(n * length + a, a, b, c) + assert np.sign(length) == orientation + + # check exceptions + with pytest.raises(Exception): + tf.orientation_point_plane(n, a, a, c) + with pytest.raises(Exception): + tf.orientation_point_plane(n, a, b, b) + with pytest.raises(Exception): + tf.orientation_point_plane(n, c, b, c) + with pytest.raises(Exception): + tf.orientation_point_plane(n, a, a, a) + + # check special case point on plane + a = np.array([1, 0, 0]) + b = np.array([0, 1, 0]) + c = np.array([0, 0, 1]) + orientation = tf.orientation_point_plane(a, a, b, c) + assert orientation == 0 # test cartesian coordinate system class -------------------------------------- From bca1b30598573fa979646661b40b4e46cebe7cf2 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Thu, 16 Jan 2020 15:50:56 +0100 Subject: [PATCH 061/177] Test is_orthogonal function --- mypackage/transformations.py | 13 ++++++++++++- tests/test_trasformations.py | 27 +++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/mypackage/transformations.py b/mypackage/transformations.py index c9ec992..7594bba 100644 --- a/mypackage/transformations.py +++ b/mypackage/transformations.py @@ -105,7 +105,18 @@ def orientation_point_plane(point, a, b, c): def is_orthogonal(u, v, tolerance=1E-9): - return np.abs(np.dot(normalize(u), normalize(v))) < tolerance + """ + Check if vectors are orthogonal. + + :param u: First vector + :param v: Second vector + :param tolerance: Numerical tolerance + :return: True or False + """ + if math.isclose(np.dot(u, u), 0) or math.isclose(np.dot(v, v), 0): + raise Exception("One or both vectors have zero length.") + + return math.isclose(np.dot(u, v), 0, abs_tol=tolerance) def change_of_base_rotation(css_from, css_to): diff --git a/tests/test_trasformations.py b/tests/test_trasformations.py index 708fe2f..3c7dfb6 100644 --- a/tests/test_trasformations.py +++ b/tests/test_trasformations.py @@ -190,6 +190,33 @@ def test_orientation_point_plane(): assert orientation == 0 +def test_is_orthogonal(): + [x, y, z] = rotated_positive_orthogonal_base() + + assert tf.is_orthogonal(x, y) + assert tf.is_orthogonal(y, x) + assert tf.is_orthogonal(y, z) + assert tf.is_orthogonal(z, y) + assert tf.is_orthogonal(z, x) + assert tf.is_orthogonal(x, z) + + assert not tf.is_orthogonal(x, x) + assert not tf.is_orthogonal(y, y) + assert not tf.is_orthogonal(z, z) + + # check tolerance is working + assert not tf.is_orthogonal(x + 0.00001, z, 1E-6) + assert tf.is_orthogonal(x + 0.00001, z, 1E-4) + + # check zero length vectors cause exception + with pytest.raises(Exception): + tf.is_orthogonal([0, 0, 0], z) + with pytest.raises(Exception): + tf.is_orthogonal(x, [0, 0, 0]) + with pytest.raises(Exception): + tf.is_orthogonal([0, 0, 0], [0, 0, 0]) + + # test cartesian coordinate system class -------------------------------------- def test_cartesian_coordinate_system_construction(): From 9d1f78978f6800401f42a7e03174ffbc7290e480 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Thu, 16 Jan 2020 17:48:54 +0100 Subject: [PATCH 062/177] Test change of base functions --- mypackage/transformations.py | 11 +++++++ tests/test_trasformations.py | 57 +++++++++++++++++++++++++++++++++--- 2 files changed, 64 insertions(+), 4 deletions(-) diff --git a/mypackage/transformations.py b/mypackage/transformations.py index 7594bba..8ae4a37 100644 --- a/mypackage/transformations.py +++ b/mypackage/transformations.py @@ -3,6 +3,8 @@ from scipy.spatial.transform import Rotation as Rot +# functions ------------------------------------------------------------------- + def rotation_matrix_x(angle): """ Create a rotation matrix that rotates around the x-axis. @@ -120,6 +122,13 @@ def is_orthogonal(u, v, tolerance=1E-9): def change_of_base_rotation(css_from, css_to): + """ + Calculate the rotatory transformation between 2 coordinate systems. + + :param css_from: Source coordinate system + :param css_to: Target coordinate system + :return: Rotation matrix + """ return np.linalg.solve(css_from.basis, css_to.basis) @@ -127,6 +136,8 @@ def change_of_base_translation(css_from, css_to): return css_from.origin - css_to.origin +# cartesian coordinate system class ------------------------------------------- + class CartesianCoordinateSystem3d: """Defines a 3d cartesian coordinate system.""" diff --git a/tests/test_trasformations.py b/tests/test_trasformations.py index 3c7dfb6..17c33aa 100644 --- a/tests/test_trasformations.py +++ b/tests/test_trasformations.py @@ -3,6 +3,7 @@ import pytest import random import math +import copy # helpers for tests ----------------------------------------------------------- @@ -41,6 +42,12 @@ def check_matrix_orthogonal(matrix): assert np.abs(product[i][j] - unit[i][j]) < 1E-9 +def check_matrix_identical(a, b): + for i in range(3): + for j in range(3): + assert math.isclose(a[i, j], b[i, j], abs_tol=1E-9) + + def is_orientation_positive(ccs): return tf.orientation_point_plane_containing_origin(ccs.basis[2], ccs.basis[0], @@ -57,15 +64,13 @@ def random_non_unit_vector(): return vec -def rotated_positive_orthogonal_base(): +def rotated_positive_orthogonal_base(angle_x=np.pi / 3, angle_y=np.pi / 4, + angle_z=np.pi / 5): x = [1, 0, 0] y = [0, 1, 0] z = [0, 0, 1] # rotate axes to produce a more general test case - angle_x = np.pi / 3 - angle_y = np.pi / 4 - angle_z = np.pi / 5 r_x = tf.rotation_matrix_x(angle_x) r_y = tf.rotation_matrix_y(angle_y) r_z = tf.rotation_matrix_z(angle_z) @@ -217,6 +222,50 @@ def test_is_orthogonal(): tf.is_orthogonal([0, 0, 0], [0, 0, 0]) +def test_change_of_base_rotation(): + diff_angle = np.pi / 2 + ref_mat = [tf.rotation_matrix_x(-diff_angle), + tf.rotation_matrix_y(-diff_angle), + tf.rotation_matrix_z(-diff_angle)] + + for i in range(3): + angles_from = np.pi * np.array([1 / 3., 1 / 5., 1 / 4]) + for j in np.arange(i + 1, 3): + angles_from[j] = 0 + + angles_to = copy.deepcopy(angles_from) + angles_to[i] += diff_angle + + base_from = rotated_positive_orthogonal_base(*angles_from) + base_to = rotated_positive_orthogonal_base(*angles_to) + + ccs_from = tf.CartesianCoordinateSystem3d(base_from, + random_non_unit_vector()) + ccs_to = tf.CartesianCoordinateSystem3d(base_to, + random_non_unit_vector()) + + matrix = tf.change_of_base_rotation(ccs_from, ccs_to) + + check_matrix_identical(matrix, ref_mat[i]) + + +def test_change_of_base_translation(): + for i in range(20): + origin_from = random_non_unit_vector() + origin_to = random_non_unit_vector() + base_from = rotated_positive_orthogonal_base(*random_non_unit_vector()) + base_to = rotated_positive_orthogonal_base(*random_non_unit_vector()) + + ccs_from = tf.CartesianCoordinateSystem3d(base_from, origin_from) + ccs_to = tf.CartesianCoordinateSystem3d(base_to, origin_to) + + diff = tf.change_of_base_translation(ccs_from, ccs_to) + + expected_diff = origin_from - origin_to + for j in range(3): + assert math.isclose(diff[j], expected_diff[j]) + + # test cartesian coordinate system class -------------------------------------- def test_cartesian_coordinate_system_construction(): From d2aa9e5f082aa5655ad901d09f27d6178c3e38e0 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Fri, 17 Jan 2020 11:18:51 +0100 Subject: [PATCH 063/177] Optimize and test linear trace segment --- mypackage/point_cloud_generator.py | 59 +++++++++++-------- tests/test_point_cloud_generator.py | 90 ++++++++++++++++++++++++++++- 2 files changed, 124 insertions(+), 25 deletions(-) diff --git a/mypackage/point_cloud_generator.py b/mypackage/point_cloud_generator.py index aff9076..2efa3a5 100644 --- a/mypackage/point_cloud_generator.py +++ b/mypackage/point_cloud_generator.py @@ -59,6 +59,11 @@ def rasterize(self, raster_width): @property def shapes(self): + """ + Get the profiles shapes. + + :return: Shapes + """ return self._shapes @@ -80,35 +85,41 @@ def is_point3d(point): return True -class LinearHorizontalTraceSegment: - - def __init__(self, point_start, point_end): - point_start = np.array(point_start) - point_end = np.array(point_end) +# Trace segment classes ------------------------------------------------------- - check_is_point2d(point_start) - check_is_point2d(point_end) +class LinearHorizontalTraceSegment: + """Trace segment with a linear path and constant z-component.""" - self._points = np.array([point_start, point_end]) - self._direction_vector = self._points[1] - self._points[0] - self._length = np.linalg.norm(self._direction_vector) + def __init__(self, length): + """ + Constructor. - def _position(self, weight): - weight = np.clip(weight, 0, 1) - current_position = self._points[0] + weight * self._direction_vector - return np.append(current_position, [0]) + :param length: Length of the segment + """ + if length <= 0: + raise ValueError("'length' must have a positive value.") + self._length = length + @property def length(self): + """ + Get the length of the segment. + + :return: Length of the segment + """ return self._length - def local_coordinate_system(self, weight): - ccs = tf.CartesianCoordinateSystem3d - weight = np.clip(weight, 0, 1) - direction = np.append(self._direction_vector, [0]) - return ccs.construct_from_yz_and_orientation(direction, - [0, 0, 1], - True, - self._position(weight)) + def local_coordinate_system(self, relative_position): + """ + Calculate a local coordinate system along the trace segment. + + :param relative_position: Relative position on the trace [0 .. 1] + :return: Local coordinate system + """ + relative_position = np.clip(relative_position, 0, 1) + + origin = np.array([0, 1, 0]) * relative_position * self._length + return tf.CartesianCoordinateSystem3d(origin=origin) class Trace: @@ -119,7 +130,7 @@ def __init__(self, self._coordinate_system = coordinate_system length = 0 for segment in self._segments: - length += segment.length() + length += segment.length self._length = length def length(self): @@ -131,7 +142,7 @@ def local_coordinate_system(self, position): cs_stack = copy.deepcopy(self._coordinate_system) for i in range(len(self._segments)): - segment_length = self._segments[i].length() + segment_length = self._segments[i].length if position <= segment_length: weight = position / segment_length else: diff --git a/tests/test_point_cloud_generator.py b/tests/test_point_cloud_generator.py index e7c2c00..25eb79b 100644 --- a/tests/test_point_cloud_generator.py +++ b/tests/test_point_cloud_generator.py @@ -2,10 +2,12 @@ import mypackage.point_cloud_generator as pcg import mypackage.geometry as geo import mypackage.transformations as tf +import math import numpy as np +import copy -# profile class --------------------------------------------------------------- +# Test profile class ---------------------------------------------------------- def test_profile_construction_and_shape_addition(): shape = geo.Shape2D([0, 0], [1, 0]) @@ -66,3 +68,89 @@ def test_profile_rasterization(): expected_raster_point_x = i * raster_width - 1 assert data[i, 0] - expected_raster_point_x < 1E-9 assert data[i, 1] == 0 + + +# Test trace segment classes -------------------------------------------------- + +def check_trace_segment_length(segment): + lcs = segment.local_coordinate_system(1) + length_numeric_prev = np.linalg.norm(lcs.origin) + + # calculate numerical length by linearization + num_segments = 2. + num_iterations = 20 + + # calculate numerical length with increasing number of segments until + # the rate of change between 2 calculations is small enough + for i in range(num_iterations): + length_numeric = 0 + increment = 1. / num_segments + + ccs0 = segment.local_coordinate_system(0) + for rel_pos in (np.arange(increment, 1. + increment / 2, increment)): + ccs1 = segment.local_coordinate_system(rel_pos) + length_numeric += np.linalg.norm(ccs1.origin - ccs0.origin) + ccs0 = copy.deepcopy(ccs1) + + relative_change = length_numeric / length_numeric_prev + + length_numeric_prev = copy.deepcopy(length_numeric) + num_segments *= 2 + + if math.isclose(relative_change, 1): + break + assert i < num_iterations - 1, "Segment length could not be " \ + "determined numerically" + + assert math.isclose(length_numeric, segment.length) + + +def are_vectors_identical(a, b, tolerance=1E-9): + if not a.size == b.size: + return False + for i in range(a.size): + if not math.isclose(a[i], b[i], abs_tol=tolerance): + return False + return True + + +def check_trace_segment_orientation(segment): + # The initial orientation of a segment must be [0, 1, 0] + lcs = segment.local_coordinate_system(0) + assert are_vectors_identical(lcs.basis[1], np.array([0, 1, 0])) + + delta = 1E-9 + for rel_pos in np.arange(0.1, 1.01, 0.1): + lcs = segment.local_coordinate_system(rel_pos) + lcs_d = segment.local_coordinate_system(rel_pos - delta) + trace_direction_numerical = tf.normalize(lcs.origin - lcs_d.origin) + + # Check that the y-axis is always aligned with the trace's direction + assert are_vectors_identical(lcs.basis[1], trace_direction_numerical, + 1E-6) + + +def default_trace_segment_tests(segment): + lcs = segment.local_coordinate_system(0) + + # test that function actually returns a coordinate system class + assert isinstance(lcs, tf.CartesianCoordinateSystem3d) + + # check that origin for weight 0 is at [0,0,0] + for i in range(3): + assert math.isclose(lcs.origin[i], 0) + + check_trace_segment_length(segment) + check_trace_segment_orientation(segment) + + +def test_linear_horizontal_trace_segment(): + length = 7.13 + segment = pcg.LinearHorizontalTraceSegment(length) + + assert math.isclose(segment.length, length) + + default_trace_segment_tests(segment) + + +test_linear_horizontal_trace_segment() From 1433f97a253d3be5820224fc5ce5b55641566f75 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Mon, 20 Jan 2020 10:38:24 +0100 Subject: [PATCH 064/177] Change coordinate system basis to coulumn major --- mypackage/point_cloud_generator.py | 71 +++++++++++++++++++++++++++-- mypackage/transformations.py | 21 +++++---- tests/test_point_cloud_generator.py | 8 ++-- tests/test_trasformations.py | 30 +++++++----- 4 files changed, 100 insertions(+), 30 deletions(-) diff --git a/mypackage/point_cloud_generator.py b/mypackage/point_cloud_generator.py index 2efa3a5..8b17a09 100644 --- a/mypackage/point_cloud_generator.py +++ b/mypackage/point_cloud_generator.py @@ -122,6 +122,67 @@ def local_coordinate_system(self, relative_position): return tf.CartesianCoordinateSystem3d(origin=origin) +class RadialHorizontalTraceSegment: + """Trace segment describing an arc with constant z-component.""" + + def __init__(self, radius, angle, clockwise=False): + """ + Constructor. + + :param radius: Radius of the arc + :param angle: Angle of the arc + :param clockwise: If True, the rotation is clockwise. Otherwise it + is counter-clockwise. + """ + if radius <= 0: + raise ValueError("'radius' must have a positive value.") + if angle <= 0: + raise ValueError("'angle' must have a positive value.") + self._radius = radius + self._angle = angle + self._length = self._arc_length(radius, angle) + if clockwise: + self._sign_winding = -1 + else: + self._sign_winding = 1 + + @staticmethod + def _arc_length(radius, angle): + """ + Calculate the arc length + :param radius: Radius + :param angle: Angle (rad) + :return: Arc length + """ + return angle * radius + + @property + def length(self): + """ + Get the length of the segment. + + :return: Length of the segment + """ + return self._length + + def local_coordinate_system(self, relative_position): + """ + Calculate a local coordinate system along the trace segment. + + :param relative_position: Relative position on the trace [0 .. 1] + :return: Local coordinate system + """ + relative_position = np.clip(relative_position, 0, 1) + + basis = tf.rotation_matrix_z( + self._angle * relative_position * self._sign_winding) + # print(basis[0]) + translation = np.array([1, 0, 0]) * self._radius * self._sign_winding + + origin = np.matmul(basis, translation) - translation + return tf.CartesianCoordinateSystem3d(basis, origin) + + class Trace: def __init__(self, segments, @@ -149,9 +210,9 @@ def local_coordinate_system(self, position): weight = 1 cs_local = self._segments[i].local_coordinate_system(weight) - tra = tf.change_of_base_translation(cs_local, cs_base) - rot = tf.change_of_base_rotation(cs_base, cs_local) - rot_tra = tf.change_of_base_rotation(cs_stack, cs_base) + tra = tf.change_of_basis_translation(cs_local, cs_base) + rot = tf.change_of_basis_rotation(cs_local, cs_base) + rot_tra = tf.change_of_basis_rotation(cs_stack, cs_base) tra = np.matmul(rot_tra, tra) @@ -271,8 +332,8 @@ def rasterize(self, raster_width): profile_raster_data = np.insert(profile_raster_data, 1, 0, axis=1) trace_cs = trace.local_coordinate_system(position) - rotation = tf.change_of_base_rotation(trace_cs, global_cs) - translation = tf.change_of_base_translation(trace_cs, global_cs) + rotation = tf.change_of_basis_rotation(trace_cs, global_cs) + translation = tf.change_of_basis_translation(trace_cs, global_cs) profile_raster_data = np.matmul(profile_raster_data, np.transpose(rotation)) diff --git a/mypackage/transformations.py b/mypackage/transformations.py index 8ae4a37..baa534f 100644 --- a/mypackage/transformations.py +++ b/mypackage/transformations.py @@ -121,19 +121,20 @@ def is_orthogonal(u, v, tolerance=1E-9): return math.isclose(np.dot(u, v), 0, abs_tol=tolerance) -def change_of_base_rotation(css_from, css_to): +def change_of_basis_rotation(ccs_from, ccs_to): """ Calculate the rotatory transformation between 2 coordinate systems. - :param css_from: Source coordinate system - :param css_to: Target coordinate system + :param ccs_from: Source coordinate system + :param ccs_to: Target coordinate system :return: Rotation matrix """ - return np.linalg.solve(css_from.basis, css_to.basis) + return np.matmul(ccs_from.basis, np.transpose(ccs_to.basis)) + # return np.linalg.solve(ccs_from.basis, ccs_to.basis) -def change_of_base_translation(css_from, css_to): - return css_from.origin - css_to.origin +def change_of_basis_translation(ccs_from, ccs_to): + return ccs_from.origin - ccs_to.origin # cartesian coordinate system class ------------------------------------------- @@ -185,7 +186,7 @@ def construct_from_xyz(cls, x, y, z, origin=np.array([0, 0, 0])): :param origin: Position of the origin :return: Cartesian coordinate system """ - basis = [x, y, z] + basis = np.transpose([x, y, z]) return cls(basis, origin=origin) @classmethod @@ -203,7 +204,7 @@ def construct_from_xy_and_orientation(cls, x, y, positive_orientation=True, """ z = cls._calcualte_orthogonal_axis(x, y) * cls._sign_orientation( positive_orientation) - basis = [x, y, z] + basis = np.transpose([x, y, z]) return cls(basis, origin=origin) @classmethod @@ -221,7 +222,7 @@ def construct_from_yz_and_orientation(cls, y, z, positive_orientation=True, """ x = cls._calcualte_orthogonal_axis(y, z) * cls._sign_orientation( positive_orientation) - basis = [x, y, z] + basis = np.transpose([x, y, z]) return cls(basis, origin=origin) @classmethod @@ -239,7 +240,7 @@ def construct_from_xz_and_orientation(cls, x, z, positive_orientation=True, """ y = cls._calcualte_orthogonal_axis(z, x) * cls._sign_orientation( positive_orientation) - basis = [x, y, z] + basis = np.transpose([x, y, z]) return cls(basis, origin=origin) @staticmethod diff --git a/tests/test_point_cloud_generator.py b/tests/test_point_cloud_generator.py index 25eb79b..174c2c8 100644 --- a/tests/test_point_cloud_generator.py +++ b/tests/test_point_cloud_generator.py @@ -148,9 +148,11 @@ def test_linear_horizontal_trace_segment(): length = 7.13 segment = pcg.LinearHorizontalTraceSegment(length) - assert math.isclose(segment.length, length) - default_trace_segment_tests(segment) + assert math.isclose(segment.length, length) -test_linear_horizontal_trace_segment() + with pytest.raises(ValueError): + pcg.LinearHorizontalTraceSegment(0) + with pytest.raises(ValueError): + pcg.LinearHorizontalTraceSegment(-4.61) diff --git a/tests/test_trasformations.py b/tests/test_trasformations.py index 17c33aa..dbdb48d 100644 --- a/tests/test_trasformations.py +++ b/tests/test_trasformations.py @@ -19,10 +19,10 @@ def check_coordinate_system(ccs, basis_expected, origin_expected, assert tf.is_orthogonal(ccs.basis[2], ccs.basis[0]) for i in range(3): - unit_vec = tf.normalize(basis_expected[i]) + unit_vec = tf.normalize(basis_expected[:, i]) # check axis orientations match - assert np.abs(np.dot(ccs.basis[i], unit_vec) - 1) < 1E-9 + assert np.abs(np.dot(ccs.basis[:, i], unit_vec) - 1) < 1E-9 # check origin correct assert np.abs(origin_expected[i] - ccs.origin[i]) < 1E-9 @@ -81,7 +81,7 @@ def rotated_positive_orthogonal_base(angle_x=np.pi / 3, angle_y=np.pi / 4, y = np.matmul(r_tot, y) z = np.matmul(r_tot, z) - return [x, y, z] + return np.transpose([x, y, z]) # test functions -------------------------------------------------------------- @@ -196,7 +196,10 @@ def test_orientation_point_plane(): def test_is_orthogonal(): - [x, y, z] = rotated_positive_orthogonal_base() + basis = rotated_positive_orthogonal_base() + x = basis[:, 0] + y = basis[:, 1] + z = basis[:, 2] assert tf.is_orthogonal(x, y) assert tf.is_orthogonal(y, x) @@ -222,7 +225,7 @@ def test_is_orthogonal(): tf.is_orthogonal([0, 0, 0], [0, 0, 0]) -def test_change_of_base_rotation(): +def test_change_of_basis_rotation(): diff_angle = np.pi / 2 ref_mat = [tf.rotation_matrix_x(-diff_angle), tf.rotation_matrix_y(-diff_angle), @@ -244,12 +247,12 @@ def test_change_of_base_rotation(): ccs_to = tf.CartesianCoordinateSystem3d(base_to, random_non_unit_vector()) - matrix = tf.change_of_base_rotation(ccs_from, ccs_to) + matrix = tf.change_of_basis_rotation(ccs_from, ccs_to) check_matrix_identical(matrix, ref_mat[i]) -def test_change_of_base_translation(): +def test_change_of_basis_translation(): for i in range(20): origin_from = random_non_unit_vector() origin_to = random_non_unit_vector() @@ -259,12 +262,12 @@ def test_change_of_base_translation(): ccs_from = tf.CartesianCoordinateSystem3d(base_from, origin_from) ccs_to = tf.CartesianCoordinateSystem3d(base_to, origin_to) - diff = tf.change_of_base_translation(ccs_from, ccs_to) + diff = tf.change_of_basis_translation(ccs_from, ccs_to) expected_diff = origin_from - origin_to for j in range(3): assert math.isclose(diff[j], expected_diff[j]) - + # test cartesian coordinate system class -------------------------------------- @@ -274,10 +277,13 @@ def test_cartesian_coordinate_system_construction(): # setup ----------------------------------------------- origin = [4, -2, 6] - [x, y, z] = rotated_positive_orthogonal_base() + basis_pos = rotated_positive_orthogonal_base() + + x = basis_pos[:, 0] + y = basis_pos[:, 1] + z = basis_pos[:, 2] - basis_pos = [x, y, z] - basis_neg = [x, y, -z] + basis_neg = np.transpose([x, y, -z]) # construction with basis ----------------------------- From 61e90c1228f08b13b99e3f1dc8e2612fe951c7bd Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Mon, 20 Jan 2020 11:04:49 +0100 Subject: [PATCH 065/177] Change coordinate system basis to coulumn major --- mypackage/transformations.py | 1 - tests/test_point_cloud_generator.py | 9 +++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/mypackage/transformations.py b/mypackage/transformations.py index baa534f..a92d72c 100644 --- a/mypackage/transformations.py +++ b/mypackage/transformations.py @@ -130,7 +130,6 @@ def change_of_basis_rotation(ccs_from, ccs_to): :return: Rotation matrix """ return np.matmul(ccs_from.basis, np.transpose(ccs_to.basis)) - # return np.linalg.solve(ccs_from.basis, ccs_to.basis) def change_of_basis_translation(ccs_from, ccs_to): diff --git a/tests/test_point_cloud_generator.py b/tests/test_point_cloud_generator.py index 174c2c8..27025e1 100644 --- a/tests/test_point_cloud_generator.py +++ b/tests/test_point_cloud_generator.py @@ -156,3 +156,12 @@ def test_linear_horizontal_trace_segment(): pcg.LinearHorizontalTraceSegment(0) with pytest.raises(ValueError): pcg.LinearHorizontalTraceSegment(-4.61) + + +def test_radial_horizontal_trace_segment(): + radius = 4.74 + angle = np.pi / 1.23 + segment_cw = pcg.RadialHorizontalTraceSegment(radius, angle, True) + segment_ccw = pcg.RadialHorizontalTraceSegment(radius, angle, False) + + default_trace_segment_tests(segment_cw) From 1218b9b830c783b44952cc101293c381fb98fa4e Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Mon, 20 Jan 2020 13:57:39 +0100 Subject: [PATCH 066/177] Add and test radial trace segment --- mypackage/point_cloud_generator.py | 18 ++++++++++ tests/test_point_cloud_generator.py | 53 ++++++++++++++++++++++++----- 2 files changed, 62 insertions(+), 9 deletions(-) diff --git a/mypackage/point_cloud_generator.py b/mypackage/point_cloud_generator.py index 8b17a09..a980d9e 100644 --- a/mypackage/point_cloud_generator.py +++ b/mypackage/point_cloud_generator.py @@ -156,6 +156,15 @@ def _arc_length(radius, angle): """ return angle * radius + @property + def angle(self): + """ + Get the angle of the segment. + + :return: Angle of the segment + """ + return self._angle + @property def length(self): """ @@ -165,6 +174,15 @@ def length(self): """ return self._length + @property + def radius(self): + """ + Get the radius of the segment. + + :return: Radius of the segment + """ + return self._radius + def local_coordinate_system(self, relative_position): """ Calculate a local coordinate system along the trace segment. diff --git a/tests/test_point_cloud_generator.py b/tests/test_point_cloud_generator.py index 27025e1..0dd70cd 100644 --- a/tests/test_point_cloud_generator.py +++ b/tests/test_point_cloud_generator.py @@ -72,7 +72,7 @@ def test_profile_rasterization(): # Test trace segment classes -------------------------------------------------- -def check_trace_segment_length(segment): +def check_trace_segment_length(segment, tolerance=1E-9): lcs = segment.local_coordinate_system(1) length_numeric_prev = np.linalg.norm(lcs.origin) @@ -97,12 +97,12 @@ def check_trace_segment_length(segment): length_numeric_prev = copy.deepcopy(length_numeric) num_segments *= 2 - if math.isclose(relative_change, 1): + if math.isclose(relative_change, 1, abs_tol=tolerance / 10): break assert i < num_iterations - 1, "Segment length could not be " \ "determined numerically" - assert math.isclose(length_numeric, segment.length) + assert math.isclose(length_numeric, segment.length, abs_tol=tolerance) def are_vectors_identical(a, b, tolerance=1E-9): @@ -117,7 +117,7 @@ def are_vectors_identical(a, b, tolerance=1E-9): def check_trace_segment_orientation(segment): # The initial orientation of a segment must be [0, 1, 0] lcs = segment.local_coordinate_system(0) - assert are_vectors_identical(lcs.basis[1], np.array([0, 1, 0])) + assert are_vectors_identical(lcs.basis[:, 1], np.array([0, 1, 0])) delta = 1E-9 for rel_pos in np.arange(0.1, 1.01, 0.1): @@ -126,11 +126,11 @@ def check_trace_segment_orientation(segment): trace_direction_numerical = tf.normalize(lcs.origin - lcs_d.origin) # Check that the y-axis is always aligned with the trace's direction - assert are_vectors_identical(lcs.basis[1], trace_direction_numerical, - 1E-6) + assert are_vectors_identical(lcs.basis[:, 1], + trace_direction_numerical, 1E-6) -def default_trace_segment_tests(segment): +def default_trace_segment_tests(segment, tolerance_length=1E-9): lcs = segment.local_coordinate_system(0) # test that function actually returns a coordinate system class @@ -140,7 +140,7 @@ def default_trace_segment_tests(segment): for i in range(3): assert math.isclose(lcs.origin[i], 0) - check_trace_segment_length(segment) + check_trace_segment_length(segment, tolerance_length) check_trace_segment_orientation(segment) @@ -148,10 +148,13 @@ def test_linear_horizontal_trace_segment(): length = 7.13 segment = pcg.LinearHorizontalTraceSegment(length) + # default tests default_trace_segment_tests(segment) + # getter tests assert math.isclose(segment.length, length) + # invalid inputs with pytest.raises(ValueError): pcg.LinearHorizontalTraceSegment(0) with pytest.raises(ValueError): @@ -164,4 +167,36 @@ def test_radial_horizontal_trace_segment(): segment_cw = pcg.RadialHorizontalTraceSegment(radius, angle, True) segment_ccw = pcg.RadialHorizontalTraceSegment(radius, angle, False) - default_trace_segment_tests(segment_cw) + # default tests + default_trace_segment_tests(segment_cw, 1E-4) + default_trace_segment_tests(segment_ccw, 1E-4) + + # getter tests + assert math.isclose(segment_cw.angle, angle) + assert math.isclose(segment_ccw.angle, angle) + assert math.isclose(segment_cw.radius, radius) + assert math.isclose(segment_ccw.radius, radius) + + # check positions + for weight in np.arange(0.1, 1, 0.1): + current_angle = angle * weight + x_exp = (1 - np.cos(current_angle)) * radius + y_exp = np.sin(current_angle) * radius + + lcs_cw = segment_cw.local_coordinate_system(weight) + lcs_ccw = segment_ccw.local_coordinate_system(weight) + + assert math.isclose(lcs_cw.origin[0], x_exp) + assert math.isclose(lcs_cw.origin[1], y_exp) + assert math.isclose(lcs_ccw.origin[0], -x_exp) + assert math.isclose(lcs_ccw.origin[1], y_exp) + + # invalid inputs + with pytest.raises(ValueError): + pcg.RadialHorizontalTraceSegment(0, np.pi) + with pytest.raises(ValueError): + pcg.RadialHorizontalTraceSegment(-0.53, np.pi) + with pytest.raises(ValueError): + pcg.RadialHorizontalTraceSegment(1, 0) + with pytest.raises(ValueError): + pcg.RadialHorizontalTraceSegment(1, -np.pi) From 70e701069b6f4f003f2a3b566d585983a9ab11f9 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Mon, 20 Jan 2020 14:41:57 +0100 Subject: [PATCH 067/177] Remove no longer needed functions --- mypackage/point_cloud_generator.py | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/mypackage/point_cloud_generator.py b/mypackage/point_cloud_generator.py index a980d9e..777ad24 100644 --- a/mypackage/point_cloud_generator.py +++ b/mypackage/point_cloud_generator.py @@ -67,24 +67,6 @@ def shapes(self): return self._shapes -def check_is_point2d(point): - if not isinstance(point, np.ndarray): - raise TypeError("Point data must be a numpy array") - if not len(point.shape) == 1: - raise ValueError("Point must be a 1d array") - if not point.size == 2: - raise ValueError("Point must have a size of 2") - return True - - -def is_point3d(point): - if not isinstance(point, np.ndarray): - return False - if not point.size == 3: - return False - return True - - # Trace segment classes ------------------------------------------------------- class LinearHorizontalTraceSegment: From 7591e44a7f1294b14acc7e5efd0d2e27bbe4151b Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Mon, 20 Jan 2020 16:24:30 +0100 Subject: [PATCH 068/177] Test trace construction --- mypackage/point_cloud_generator.py | 85 ++++++++++++++++++++++++----- tests/helpers.py | 32 +++++++++++ tests/test_point_cloud_generator.py | 83 ++++++++++++++++++++++++---- 3 files changed, 173 insertions(+), 27 deletions(-) create mode 100644 tests/helpers.py diff --git a/mypackage/point_cloud_generator.py b/mypackage/point_cloud_generator.py index 777ad24..4a2214a 100644 --- a/mypackage/point_cloud_generator.py +++ b/mypackage/point_cloud_generator.py @@ -4,9 +4,20 @@ import numpy as np import copy import mypackage.transformations as tf -from scipy.spatial.transform import Rotation as R +# Helper functions ------------------------------------------------------------ + +def to_list(var): + if isinstance(var, list): + return var + if var is None: + return [] + return [var] + + +# Profile class --------------------------------------------------------------- + class Profile: """Defines a 2d profile.""" @@ -165,6 +176,9 @@ def radius(self): """ return self._radius + def is_clockwise(self): + return self._sign_winding < 0 + def local_coordinate_system(self, relative_position): """ Calculate a local coordinate system along the trace segment. @@ -176,27 +190,76 @@ def local_coordinate_system(self, relative_position): basis = tf.rotation_matrix_z( self._angle * relative_position * self._sign_winding) - # print(basis[0]) translation = np.array([1, 0, 0]) * self._radius * self._sign_winding origin = np.matmul(basis, translation) - translation return tf.CartesianCoordinateSystem3d(basis, origin) +# Trace class ----------------------------------------------------------------- + class Trace: - def __init__(self, - segments, + """Defines a 3d trace.""" + + def __init__(self, segments, coordinate_system=tf.CartesianCoordinateSystem3d()): - self._segments = segments + """ + Constructor. + + :param segments: Single segment or list of segments + :param coordinate_system: Coordinate system of the trace + """ + if not isinstance(coordinate_system, tf.CartesianCoordinateSystem3d): + raise TypeError( + "'coordinate_system' must be of type " + "transformations.CartesianCoordinateSystem3d") + + self._segments = to_list(segments) self._coordinate_system = coordinate_system + length = 0 for segment in self._segments: length += segment.length + if length <= 0: + raise Exception("Trace has no length.") + self._length = length + @property + def coordinate_system(self): + """ + Get the trace's coordinate system. + + :return: Coordinate system of the trace + """ + return self._coordinate_system + + @property def length(self): + """ + Get the length of the trace. + + :return: Length of the trace. + """ return self._length + @property + def segments(self): + """ + Get the trace's segments. + + :return: Segments of the trace + """ + return self._segments + + def num_segments(self): + """ + Get the number of segments. + + :return: Number of segments + """ + return len(self._segments) + def local_coordinate_system(self, position): position = np.clip(position, 0, self._length) cs_base = tf.CartesianCoordinateSystem3d() @@ -263,14 +326,6 @@ def interpolate(a, b, weight): return Profile(shapes) -def to_list(var): - if isinstance(var, list): - return var - if var is None: - return [] - return [var] - - class Section: """Defines a section""" @@ -299,7 +354,7 @@ def __init__(self, profiles, trace, profile_interpolations=None, self._profiles = profiles self._interpolations = profile_interpolations - self._positions = [0] + profile_positions + [trace.length()] + self._positions = [0] + profile_positions + [trace.length] self.trace = trace def _profile_segment_index(self, position): @@ -326,7 +381,7 @@ def rasterize(self, raster_width): raster_data = np.empty([0, 3]) trace = self.trace global_cs = tf.CartesianCoordinateSystem3d() - for position in np.arange(0, trace.length() + 0.01, 0.5): + for position in np.arange(0, trace.length + 0.01, 0.5): profile = self._interpolated_profile(position) profile_raster_data = profile.rasterize(raster_width) profile_raster_data = np.insert(profile_raster_data, 1, 0, axis=1) diff --git a/tests/helpers.py b/tests/helpers.py new file mode 100644 index 0000000..e17ef1a --- /dev/null +++ b/tests/helpers.py @@ -0,0 +1,32 @@ +import mypackage.transformations as tf +import numpy as np +import math + + +def check_vectors_identical(a, b, tolerance=1E-9): + assert a.size == b.size + for i in range(a.size): + assert math.isclose(a[i], b[i], abs_tol=tolerance) + + +def check_matrices_identical(a, b): + assert a.shape == b.shape + for i in range(3): + for j in range(3): + assert math.isclose(a[i, j], b[i, j], abs_tol=1E-9) + + +def rotated_coordinate_system(angle_x=np.pi / 3, angle_y=np.pi / 4, + angle_z=np.pi / 5, origin=np.array([0, 0, 0])): + basis = np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]]) + + # rotate axes to produce a more general test case + r_x = tf.rotation_matrix_x(angle_x) + r_y = tf.rotation_matrix_y(angle_y) + r_z = tf.rotation_matrix_z(angle_z) + + r_tot = np.matmul(r_z, np.matmul(r_y, r_x)) + + rotated_basis = np.matmul(r_tot, basis) + + return tf.CartesianCoordinateSystem3d(rotated_basis, np.array(origin)) diff --git a/tests/test_point_cloud_generator.py b/tests/test_point_cloud_generator.py index 0dd70cd..4306939 100644 --- a/tests/test_point_cloud_generator.py +++ b/tests/test_point_cloud_generator.py @@ -5,6 +5,8 @@ import math import numpy as np import copy +import tests.helpers as helpers +import types # Test profile class ---------------------------------------------------------- @@ -105,19 +107,10 @@ def check_trace_segment_length(segment, tolerance=1E-9): assert math.isclose(length_numeric, segment.length, abs_tol=tolerance) -def are_vectors_identical(a, b, tolerance=1E-9): - if not a.size == b.size: - return False - for i in range(a.size): - if not math.isclose(a[i], b[i], abs_tol=tolerance): - return False - return True - - def check_trace_segment_orientation(segment): # The initial orientation of a segment must be [0, 1, 0] lcs = segment.local_coordinate_system(0) - assert are_vectors_identical(lcs.basis[:, 1], np.array([0, 1, 0])) + helpers.check_vectors_identical(lcs.basis[:, 1], np.array([0, 1, 0])) delta = 1E-9 for rel_pos in np.arange(0.1, 1.01, 0.1): @@ -126,8 +119,8 @@ def check_trace_segment_orientation(segment): trace_direction_numerical = tf.normalize(lcs.origin - lcs_d.origin) # Check that the y-axis is always aligned with the trace's direction - assert are_vectors_identical(lcs.basis[:, 1], - trace_direction_numerical, 1E-6) + helpers.check_vectors_identical(lcs.basis[:, 1], + trace_direction_numerical, 1E-6) def default_trace_segment_tests(segment, tolerance_length=1E-9): @@ -176,6 +169,8 @@ def test_radial_horizontal_trace_segment(): assert math.isclose(segment_ccw.angle, angle) assert math.isclose(segment_cw.radius, radius) assert math.isclose(segment_ccw.radius, radius) + assert segment_cw.is_clockwise() is True + assert segment_ccw.is_clockwise() is False # check positions for weight in np.arange(0.1, 1, 0.1): @@ -200,3 +195,67 @@ def test_radial_horizontal_trace_segment(): pcg.RadialHorizontalTraceSegment(1, 0) with pytest.raises(ValueError): pcg.RadialHorizontalTraceSegment(1, -np.pi) + + +# Test trace class ------------------------------------------------------------ + +def test_trace_construction(): + linear_segment = pcg.LinearHorizontalTraceSegment(1) + radial_segment = pcg.RadialHorizontalTraceSegment(1, np.pi) + ccs_origin = np.array([2, 3, -2]) + ccs = helpers.rotated_coordinate_system(origin=ccs_origin) + + # test single segment construction -------------------- + trace = pcg.Trace(linear_segment, ccs) + assert math.isclose(trace.length, linear_segment.length) + assert trace.num_segments() == 1 + + segments = trace.segments + assert len(segments) == 1 + assert isinstance(segments[0], type(linear_segment)) + assert math.isclose(linear_segment.length, segments[0].length) + + helpers.check_matrices_identical(ccs.basis, trace.coordinate_system.basis) + helpers.check_vectors_identical(ccs.origin, trace.coordinate_system.origin) + + # test multi segment construction --------------------- + trace = pcg.Trace([radial_segment, linear_segment]) + assert math.isclose(trace.length, + linear_segment.length + radial_segment.length) + assert trace.num_segments() == 2 + + segments = trace.segments + assert len(segments) == 2 + assert isinstance(segments[0], type(radial_segment)) + assert isinstance(segments[1], type(linear_segment)) + + assert math.isclose(radial_segment.radius, segments[0].radius) + assert math.isclose(radial_segment.angle, segments[0].angle) + assert math.isclose(radial_segment.is_clockwise(), + segments[0].is_clockwise()) + assert math.isclose(linear_segment.length, segments[1].length) + + helpers.check_matrices_identical(np.identity(3), + trace.coordinate_system.basis) + helpers.check_vectors_identical(np.array([0, 0, 0]), + trace.coordinate_system.origin) + + # check invalid inputs -------------------------------- + with pytest.raises(TypeError): + pcg.Trace(radial_segment, linear_segment) + with pytest.raises(TypeError): + pcg.Trace(radial_segment, 2) + with pytest.raises(Exception): + pcg.Trace(None) + + # check construction with custom segment -------------- + custom_segment = types.SimpleNamespace() + custom_segment.length = 3 + pcg.Trace(custom_segment) + + with pytest.raises(Exception): + custom_segment.length = -12 + pcg.Trace(custom_segment) + with pytest.raises(Exception): + custom_segment.length = 0 + pcg.Trace(custom_segment) From c48e9752e7033b621968414853005da3549cb662 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Mon, 20 Jan 2020 17:03:11 +0100 Subject: [PATCH 069/177] Add test case to profile test --- tests/test_point_cloud_generator.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/test_point_cloud_generator.py b/tests/test_point_cloud_generator.py index 4306939..a27bdc8 100644 --- a/tests/test_point_cloud_generator.py +++ b/tests/test_point_cloud_generator.py @@ -48,6 +48,27 @@ def test_profile_construction_and_shape_addition(): profile.add_shapes([shape, shape]) assert profile.num_shapes() == 5 + # Check shapes + shapes_profile = profile.shapes + for shape_profile in shapes_profile: + assert shape.num_segments() == shape_profile.num_segments() + + segments = shape.segments + segments_profile = shape_profile.segments + + assert len(segments) == shape.num_segments() + assert len(segments) == len(segments_profile) + + for i in range(shape.num_segments()): + assert isinstance(segments_profile[i], type(segments[i])) + + points = shape.points + points_profile = shape_profile.points + + assert points.shape == points_profile.shape + for i in range(shape.num_points()): + helpers.check_vectors_identical(points[i], points_profile[i]) + def test_profile_rasterization(): raster_width = 0.1 From 4e36921ae159fdabf311f7f0fd9b06b5dc79d2f6 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Tue, 21 Jan 2020 15:15:25 +0100 Subject: [PATCH 070/177] Test local coordinate system of trace --- mypackage/point_cloud_generator.py | 93 ++++++++++++++++++----------- mypackage/transformations.py | 42 ++++++++++--- tests/test_point_cloud_generator.py | 72 +++++++++++++++++++++- tests/test_trasformations.py | 10 ++++ 4 files changed, 173 insertions(+), 44 deletions(-) diff --git a/mypackage/point_cloud_generator.py b/mypackage/point_cloud_generator.py index 4a2214a..2ab2086 100644 --- a/mypackage/point_cloud_generator.py +++ b/mypackage/point_cloud_generator.py @@ -215,15 +215,50 @@ def __init__(self, segments, "transformations.CartesianCoordinateSystem3d") self._segments = to_list(segments) - self._coordinate_system = coordinate_system + self._create_lookups(coordinate_system) - length = 0 - for segment in self._segments: - length += segment.length - if length <= 0: + if self.length <= 0: raise Exception("Trace has no length.") - self._length = length + def _create_lookups(self, coordinate_system_start): + """ + Create lookup tables. + + :param coordinate_system_start: Coordinate system at the start of + the trace. + :return: --- + """ + self._coordinate_system_lookup = [coordinate_system_start] + self._total_length_lookup = [0] + self._segment_length_lookup = [] + + segments = self._segments + + # Fill coordinate system lookup + for i in range(len(segments) - 1): + lcs_segment_end = segments[i].local_coordinate_system(1) + cs = self._coordinate_system_lookup[i] + lcs_segment_end + self._coordinate_system_lookup += [cs] + + # Fill length lookups + total_length = 0 + for i in range(len(segments)): + segment_length = segments[i].length + total_length += segment_length + self._segment_length_lookup += [segment_length] + self._total_length_lookup += [total_length] + + def _get_segment_index(self, position): + """ + Get the segment index for a certain position. + + :param position: Position + :return: Segment index + """ + position = np.clip(position, 0, self.length) + for i in range(len(self._total_length_lookup) - 1): + if position <= self._total_length_lookup[i + 1]: + return i @property def coordinate_system(self): @@ -232,7 +267,7 @@ def coordinate_system(self): :return: Coordinate system of the trace """ - return self._coordinate_system + return self._coordinate_system_lookup[0] @property def length(self): @@ -241,7 +276,7 @@ def length(self): :return: Length of the trace. """ - return self._length + return self._total_length_lookup[-1] @property def segments(self): @@ -261,32 +296,22 @@ def num_segments(self): return len(self._segments) def local_coordinate_system(self, position): - position = np.clip(position, 0, self._length) - cs_base = tf.CartesianCoordinateSystem3d() - cs_stack = copy.deepcopy(self._coordinate_system) - - for i in range(len(self._segments)): - segment_length = self._segments[i].length - if position <= segment_length: - weight = position / segment_length - else: - weight = 1 - - cs_local = self._segments[i].local_coordinate_system(weight) - tra = tf.change_of_basis_translation(cs_local, cs_base) - rot = tf.change_of_basis_rotation(cs_local, cs_base) - rot_tra = tf.change_of_basis_rotation(cs_stack, cs_base) - - tra = np.matmul(rot_tra, tra) - - basis = np.matmul(rot, cs_stack.basis) - origin = cs_stack.origin + tra - cs_stack = tf.CartesianCoordinateSystem3d(basis=basis, - origin=origin) - - if position <= segment_length: - return cs_stack - position -= segment_length + """ + Get the local coordinate system at a specific position on the trace. + + :param position: Position + :return: Local coordinate system + """ + idx = self._get_segment_index(position) + + total_length_start = self._total_length_lookup[idx] + segment_length = self._segment_length_lookup[idx] + weight = (position - total_length_start) / segment_length + + segment_cs = self.segments[idx].local_coordinate_system(weight) + start_cs = self._coordinate_system_lookup[idx] + + return start_cs + segment_cs class ProfileInterpolationLSBS: diff --git a/mypackage/transformations.py b/mypackage/transformations.py index a92d72c..655c1f1 100644 --- a/mypackage/transformations.py +++ b/mypackage/transformations.py @@ -150,19 +150,45 @@ def __init__(self, basis=np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]]), :param origin: Position of the origin :return: Cartesian coordinate system """ - basis[0] = normalize(basis[0]) - basis[1] = normalize(basis[1]) - basis[2] = normalize(basis[2]) - - if not (is_orthogonal(basis[0], basis[1]) and - is_orthogonal(basis[1], basis[2]) and - is_orthogonal(basis[2], basis[0])): + basis = np.array(basis, dtype=float) + basis[:, 0] = normalize(basis[:, 0]) + basis[:, 1] = normalize(basis[:, 1]) + basis[:, 2] = normalize(basis[:, 2]) + + if not (is_orthogonal(basis[:, 0], basis[:, 1]) and + is_orthogonal(basis[:, 1], basis[:, 2]) and + is_orthogonal(basis[:, 2], basis[:, 0])): raise Exception("Basis vectors must be orthogonal") self._basis = basis self._origin = np.array(origin) + def __add__(self, rhs_cs): + """ + Add 2 coordinate systems. + + Generates a new coordinate system by treating the right-hand side + coordinate system as being defined in the left hand-side coordinate + system. + The transformations from the base coordinate system to the new + coordinate system are equivalent to the combination of the + transformations from both added coordinate systems: + + R_n = R_l * R_r + T_n = R_l * T_r + T_l + + R_r and T_r are rotation matrix and translation vector of the + right-hand side coordinate system, R_l and T_l of the left-hand side + coordinate system and R_n and T_n of the resulting coordinate system. + + :param rhs_cs: Right-hand side coordinate system + :return: Resulting coordinate system. + """ + basis = np.matmul(self.basis, rhs_cs.basis) + origin = np.matmul(self.basis, rhs_cs.origin) + self.origin + return CartesianCoordinateSystem3d(basis, origin) + @classmethod def construct_from_basis(cls, basis, origin=np.array([0, 0, 0])): """ @@ -221,7 +247,7 @@ def construct_from_yz_and_orientation(cls, y, z, positive_orientation=True, """ x = cls._calcualte_orthogonal_axis(y, z) * cls._sign_orientation( positive_orientation) - basis = np.transpose([x, y, z]) + basis = np.transpose(np.array([x, y, z])) return cls(basis, origin=origin) @classmethod diff --git a/tests/test_point_cloud_generator.py b/tests/test_point_cloud_generator.py index a27bdc8..f4dc27c 100644 --- a/tests/test_point_cloud_generator.py +++ b/tests/test_point_cloud_generator.py @@ -6,7 +6,6 @@ import numpy as np import copy import tests.helpers as helpers -import types # Test profile class ---------------------------------------------------------- @@ -270,7 +269,11 @@ def test_trace_construction(): pcg.Trace(None) # check construction with custom segment -------------- - custom_segment = types.SimpleNamespace() + class CustomSegment(): + def local_coordinate_system(self, *args): + return tf.CartesianCoordinateSystem3d + + custom_segment = CustomSegment() custom_segment.length = 3 pcg.Trace(custom_segment) @@ -280,3 +283,68 @@ def test_trace_construction(): with pytest.raises(Exception): custom_segment.length = 0 pcg.Trace(custom_segment) + + +def test_trace_local_coordinate_system(): + radial_segment = pcg.RadialHorizontalTraceSegment(1, np.pi) + linear_segment = pcg.LinearHorizontalTraceSegment(1) + + # check with default coordinate system ---------------- + trace = pcg.Trace([radial_segment, linear_segment]) + + # check first segment + for i in range(11): + weight = i / 10 + position = radial_segment.length * weight + cs_trace = trace.local_coordinate_system(position) + cs_segment = radial_segment.local_coordinate_system(weight) + + helpers.check_matrices_identical(cs_trace.basis, cs_segment.basis) + helpers.check_vectors_identical(cs_trace.origin, cs_segment.origin) + + # check second segment + expected_basis = radial_segment.local_coordinate_system(1).basis + for i in range(11): + weight = i / 10 + position_on_segment = linear_segment.length * weight + position = radial_segment.length + position_on_segment + + expected_origin = np.array([-2, -position_on_segment, 0]) + cs_trace = trace.local_coordinate_system(position) + + helpers.check_matrices_identical(cs_trace.basis, expected_basis) + helpers.check_vectors_identical(cs_trace.origin, expected_origin) + + # check with arbitrary coordinate system -------------- + basis = tf.rotation_matrix_x(np.pi / 2) + origin = np.array([-3, 2.5, 5]) + cs_base = tf.CartesianCoordinateSystem3d(basis, origin) + + trace = pcg.Trace([radial_segment, linear_segment], cs_base) + + # check first segment + for i in range(11): + weight = i / 10 + position = radial_segment.length * weight + cs_trace = trace.local_coordinate_system(position) + cs_segment = radial_segment.local_coordinate_system(weight) + + expected_basis = np.matmul(basis, cs_segment.basis) + expected_origin = np.matmul(basis, cs_segment.origin) + origin + + helpers.check_matrices_identical(cs_trace.basis, expected_basis) + helpers.check_vectors_identical(cs_trace.origin, expected_origin) + + # check second segment + expected_basis = np.matmul(basis, + radial_segment.local_coordinate_system(1).basis) + for i in range(11): + weight = i / 10 + position_on_segment = linear_segment.length * weight + position = radial_segment.length + position_on_segment + + expected_origin = np.array([-2, 0, -position_on_segment]) + origin + cs_trace = trace.local_coordinate_system(position) + + helpers.check_matrices_identical(cs_trace.basis, expected_basis) + helpers.check_vectors_identical(cs_trace.origin, expected_origin) diff --git a/tests/test_trasformations.py b/tests/test_trasformations.py index dbdb48d..a73a36c 100644 --- a/tests/test_trasformations.py +++ b/tests/test_trasformations.py @@ -325,6 +325,16 @@ def test_cartesian_coordinate_system_construction(): check_coordinate_system(ccs_xzo_pos, basis_pos, origin, True) check_coordinate_system(ccs_xzo_neg, basis_neg, origin, False) + # test integers as inputs ----------------------------- + x_i = [1, 1, 0] + y_i = [-1, 1, 0] + z_i = [0, 0, 1] + + cls_ccs.construct_from_xyz(x_i, y_i, z_i, origin) + cls_ccs.construct_from_xy_and_orientation(x_i, y_i) + cls_ccs.construct_from_yz_and_orientation(y_i, z_i) + cls_ccs.construct_from_xz_and_orientation(z_i, x_i) + # check exceptions ------------------------------------ with pytest.raises(Exception): cls_ccs([x, y, [0, 0, 1]]) From 59c7cc47d8c7e565c2d7c1518400d110b3464bbc Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Tue, 21 Jan 2020 16:43:30 +0100 Subject: [PATCH 071/177] Test coordinate system addition --- tests/test_trasformations.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/test_trasformations.py b/tests/test_trasformations.py index a73a36c..011cd49 100644 --- a/tests/test_trasformations.py +++ b/tests/test_trasformations.py @@ -4,6 +4,7 @@ import random import math import copy +import tests.helpers as helper # helpers for tests ----------------------------------------------------------- @@ -338,3 +339,31 @@ def test_cartesian_coordinate_system_construction(): # check exceptions ------------------------------------ with pytest.raises(Exception): cls_ccs([x, y, [0, 0, 1]]) + + +def test_cartesian_coordinate_system_addition(): + cls_ccs = tf.CartesianCoordinateSystem3d + + orientation0 = tf.rotation_matrix_z(np.pi / 2) + origin0 = [1, 3, 2] + ccs0 = cls_ccs(orientation0, origin0) + + orientation1 = tf.rotation_matrix_y(np.pi / 2) + origin1 = [4, -2, 1] + ccs1 = cls_ccs(orientation1, origin1) + + orientation2 = tf.rotation_matrix_x(np.pi / 2) + origin2 = [-3, 4, 2] + ccs2 = cls_ccs(orientation2, origin2) + + # check i + ccs_tot_0 = ccs0 + (ccs1 + ccs2) + ccs_tot_1 = (ccs0 + ccs1) + ccs2 + helper.check_matrices_identical(ccs_tot_0.basis, ccs_tot_1.basis) + helper.check_vectors_identical(ccs_tot_0.origin, ccs_tot_1.origin) + + expected_origin = np.array([-1, 9, 6]) + expected_orientation = np.array([[0, 0, 1], [0, 1, 0], [-1, 0, 0]]) + + helper.check_matrices_identical(ccs_tot_0.basis, expected_orientation) + helper.check_vectors_identical(ccs_tot_0.origin, expected_origin) From 135341163f3d991c8c78b7085418b0b503065651 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Wed, 22 Jan 2020 11:56:52 +0100 Subject: [PATCH 072/177] Add new line segment class --- mypackage/geometry.py | 67 ++++++++++++++++++++++++++++ mypackage/point_cloud_generator.py | 19 +++++--- tests/test_geometry.py | 68 +++++++++++++++++++++++++++++ tests/test_point_cloud_generator.py | 25 +++++++++++ 4 files changed, 174 insertions(+), 5 deletions(-) diff --git a/mypackage/geometry.py b/mypackage/geometry.py index 6a6828f..e092bdb 100644 --- a/mypackage/geometry.py +++ b/mypackage/geometry.py @@ -1,6 +1,7 @@ """Provides classes to define lines and surfaces.""" import numpy as np +import math from scipy.spatial.transform import Rotation as R @@ -83,6 +84,72 @@ def reflection_multiplier(transformation_matrix): return np.sign(origin_left_of_line) +class LineSegment: + """Line segment.""" + + def __init__(self, point_start, point_end): + """ + Constructor. + + :param point_start: Starting point of the segment + :param point_end: End point of the segment + """ + self._points = np.transpose( + np.array([point_start, point_end], dtype=float)) + self._calculate_length() + + def _calculate_length(self): + """ + Calculate the segment length from its points. + + :return: --- + """ + self._length = np.linalg.norm(self._points[:, 1] - self._points[:, 0]) + if math.isclose(self._length, 0): + raise ValueError("Segment length is 0.") + + @property + def length(self): + """ + Get the segment length. + + :return: Segment length + """ + return self._length + + def rasterize(self, raster_width, num_points_excluded_end=0): + """ + Create an array of points that describe the segments contour. + + The effective raster width may vary from the specified one, + since the algorithm enforces constant distances between two + raster points. + + :param raster_width: The desired distance between two raster points + :param num_points_excluded_end: Specifies how many points from the + end should be excluded from the rasterization. The main purpose of + this parameter is to avoid point duplication when rasterizing + multiple segments. + :return: Array of contour points + """ + raster_width = np.clip(np.abs(raster_width), 0, self.length) + if not raster_width > 0: + raise ValueError("'raster_width' is zero") + + num_raster_segments = np.round(self.length / raster_width) + + # normalized effective raster width + nerw = 1. / num_raster_segments + + range_modifier = (0.5 - np.floor( + np.abs(num_points_excluded_end))) * nerw + + multiplier = np.arange(0, 1 + range_modifier, nerw) + weight_matrix = np.array([1 - multiplier, multiplier]) + + return np.matmul(self._points, weight_matrix) + + class Shape2D: """Defines a shape in 2 dimensions.""" diff --git a/mypackage/point_cloud_generator.py b/mypackage/point_cloud_generator.py index 2ab2086..78a14ab 100644 --- a/mypackage/point_cloud_generator.py +++ b/mypackage/point_cloud_generator.py @@ -308,17 +308,26 @@ def local_coordinate_system(self, position): segment_length = self._segment_length_lookup[idx] weight = (position - total_length_start) / segment_length - segment_cs = self.segments[idx].local_coordinate_system(weight) - start_cs = self._coordinate_system_lookup[idx] + local_segment_cs = self.segments[idx].local_coordinate_system(weight) + segment_start_cs = self._coordinate_system_lookup[idx] - return start_cs + segment_cs + return segment_start_cs + local_segment_cs -class ProfileInterpolationLSBS: - """Linear segment by segment profile interpolation class.""" +class LinearProfileInterpolationSBS: + """Linear segment by segment interpolation class for profiles.""" @staticmethod def interpolate(a, b, weight): + """ + Interpolate 2 profiles. + + :param a: First profile + :param b: Second profile + :param weight: Weighting factor [0 .. 1]. If 0, the profile is + identical to 'a' and if 1, it is identical to b. + :return: Interpolated profile + """ weight = np.clip(weight, 0, 1) if not len(a.shapes) == len(b.shapes): raise Exception("Number of profile shapes do not match.") diff --git a/tests/test_geometry.py b/tests/test_geometry.py index 5ffa0dd..bacc61e 100644 --- a/tests/test_geometry.py +++ b/tests/test_geometry.py @@ -1,7 +1,9 @@ import pytest import mypackage.geometry as geo import numpy as np +import math import copy +import tests.helpers as helper def test_vector_points_to_left_of_vector(): @@ -64,6 +66,72 @@ def test_reflection_multiplier(): geo.reflection_multiplier([[2, 2], [1, 1]]) +# test LineSegment ------------------------------------------------------------ + +def test_line_segment(): + segment = geo.LineSegment([3, 3], [4, 5]) + assert math.isclose(segment.length, np.sqrt(5)) + + with pytest.raises(ValueError): + geo.LineSegment([0, 1], [0, 1]) + + +def test_line_segment_rasterization(): + point_start = np.array([3, 3]) + point_end = np.array([4, 5]) + vec_start_end = point_end - point_start + unit_vec_start_end = vec_start_end / np.linalg.norm(vec_start_end) + + segment = geo.LineSegment(point_start, point_end) + raster_data = segment.rasterize(0.1) + + shape = raster_data.shape + assert len(shape) == 2 + + point_size = shape[0] + num_points = shape[1] + assert point_size == 2 + + for i in range(num_points): + point = raster_data[:, i] + + if i == 0: + helper.check_vectors_identical(point, point_start) + elif i == num_points - 1: + helper.check_vectors_identical(point, point_end) + else: + vec_start_point = point - point_start + unit_vec_start_point = vec_start_point / np.linalg.norm( + vec_start_point) + + assert math.isclose( + np.dot(unit_vec_start_point, unit_vec_start_end), + 1) + + # check rasterization with excluded points + raster_data_m2 = segment.rasterize(0.1, 2) + + num_points_m2 = raster_data_m2.shape[1] + + assert num_points - 2 == num_points_m2 + for i in range(num_points_m2): + helper.check_vectors_identical(raster_data[:, i], raster_data_m2[:, i]) + + # check that rasterization with to large raster width still works + raster_data_200 = segment.rasterize(200) + + num_points_200 = raster_data_200.shape[1] + assert num_points_200 == 2 + helper.check_vectors_identical(point_start, raster_data_200[:, 0]) + helper.check_vectors_identical(point_end, raster_data_200[:, 1]) + + # check exceptions + with pytest.raises(ValueError): + segment.rasterize(0) + + +# test Shape2d ---------------------------------------------------------------- + def test_shape2d_construction(): # Test Exception: Segment length too small with pytest.raises(Exception): diff --git a/tests/test_point_cloud_generator.py b/tests/test_point_cloud_generator.py index f4dc27c..b96aa9f 100644 --- a/tests/test_point_cloud_generator.py +++ b/tests/test_point_cloud_generator.py @@ -348,3 +348,28 @@ def test_trace_local_coordinate_system(): helpers.check_matrices_identical(cs_trace.basis, expected_basis) helpers.check_vectors_identical(cs_trace.origin, expected_origin) + + +# Profile interpolation classes ----------------------------------------------- + +def test_linear_profile_interpolation_sbs(): + interpolator = pcg.LinearProfileInterpolationSBS + + a_0 = [0, 0] + a_1 = [8, 16] + a_2 = [16, 0] + shape_a0 = geo.Shape2D(a_0, a_1) + shape_a0 = geo.Shape2D(a_0, a_1) + + b_0 = [-4, 8] + b_1 = [0, 8] + b_2 = [16, -16] + shape_b0 = geo.Shape2D(b_0, b_1) + shape_b0 = geo.Shape2D(b_0, b_1) + + # check weight 0 gives a + # profile_c = interpolator.interpolate(profile_a, profile_b, 0) + + # exp0 = [-1, 2] + # exp1 = [2, 14] + # exp2 = [16, -4] From b13bcbb314dd9074d3b10721a12b5ebc1bc14d45 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Wed, 22 Jan 2020 14:31:29 +0100 Subject: [PATCH 073/177] Add new arc segment --- mypackage/geometry.py | 110 +++++++++++++++++++++++++++++++++++++++++ tests/test_geometry.py | 32 ++++++++++++ 2 files changed, 142 insertions(+) diff --git a/mypackage/geometry.py b/mypackage/geometry.py index e092bdb..d6c8236 100644 --- a/mypackage/geometry.py +++ b/mypackage/geometry.py @@ -2,6 +2,7 @@ import numpy as np import math +import mypackage.transformations as tf from scipy.spatial.transform import Rotation as R @@ -84,6 +85,8 @@ def reflection_multiplier(transformation_matrix): return np.sign(origin_left_of_line) +# Segments -------------------------------------------------------------------- + class LineSegment: """Line segment.""" @@ -150,6 +153,113 @@ def rasterize(self, raster_width, num_points_excluded_end=0): return np.matmul(self._points, weight_matrix) +class ArcSegment: + """Arc segment.""" + + def __init__(self, point_start, point_end, point_center, + arc_winding_ccw=True): + """ + Constructor. + + :param point_start: Starting point of the segment + :param point_end: End point of the segment + :param point_center: Center point of the arc + :param: arc_winding_ccw: Specifies if the arcs winding order is + counter-clockwise + """ + if arc_winding_ccw: + self._sign_arc_winding = 1 + else: + self._sign_arc_winding = -1 + + self._points = np.transpose( + np.array([point_start, point_end, point_center], dtype=float)) + self._radius = np.linalg.norm(self._points[:, 0] - self._points[:, 2]) + self._calculate_arc_angle() + self._arc_length = self._arc_angle * self._radius + + self._check_valid() + + def _calculate_arc_angle(self): + """ + Calculate the arc angle. + + :return: --- + """ + point_start = self._points[:, 0] + point_end = self._points[:, 1] + point_center = self._points[:, 2] + + # Calculate angle between vectors (always the smaller one) + unit_center_start = tf.normalize(point_start - point_center) + unit_center_end = tf.normalize(point_end - point_center) + + dot_unit = np.dot(unit_center_start, unit_center_end) + angle_vecs = np.arccos(np.clip(dot_unit, -1, 1)) + + sign_winding_points = vector_points_to_left_of_vector( + unit_center_end, unit_center_start) + + if np.abs(sign_winding_points + self._sign_arc_winding) > 0: + self._arc_angle = angle_vecs + else: + self._arc_angle = 2 * np.pi - angle_vecs + + def _check_valid(self): + """ + Check if the segments data is valid. + :return: --- + """ + point_start = self._points[:, 0] + point_end = self._points[:, 1] + point_center = self._points[:, 2] + + vec_start_center = np.linalg.norm(point_start - point_center) + vec_end_center = np.linalg.norm(point_end - point_center) + + if not math.isclose(vec_end_center - vec_start_center, 0): + raise ValueError("Radius is not constant.") + if math.isclose(self._radius, 0): + raise Exception("Radius is 0.") + if math.isclose(self._arc_length, 0): + raise Exception("Arc length is 0.") + + @property + def arc_angle(self): + """ + Get the arc angle. + + :return: Arc angle + """ + return self._arc_angle + + @property + def arc_length(self): + """ + Get the arc length. + + :return: Arc length + """ + return self._arc_length + + @property + def radius(self): + """ + Get the radius. + + :return: Radius + """ + return self._radius + + def is_arc_winding_ccw(self): + """ + Get True if the winding order is counter-clockwise. False if clockwise. + + :return: True or False + """ + return self._sign_arc_winding > 0 + + class Shape2D: """Defines a shape in 2 dimensions.""" diff --git a/tests/test_geometry.py b/tests/test_geometry.py index bacc61e..7e8d456 100644 --- a/tests/test_geometry.py +++ b/tests/test_geometry.py @@ -130,6 +130,38 @@ def test_line_segment_rasterization(): segment.rasterize(0) +# test ArcSegment ------------------------------------------------------------ + +def test_arc_segment_construction(): + segment_cw = geo.ArcSegment([3, 3], [6, 6], [6, 3], False) + segment_ccw = geo.ArcSegment([3, 3], [6, 6], [6, 3], True) + + assert not segment_cw.is_arc_winding_ccw() + assert segment_ccw.is_arc_winding_ccw() + + assert math.isclose(segment_cw.radius, 3) + assert math.isclose(segment_ccw.radius, 3) + + assert math.isclose(segment_cw.arc_angle, 1 / 2 * np.pi) + assert math.isclose(segment_ccw.arc_angle, 3 / 2 * np.pi) + + assert math.isclose(segment_cw.arc_length, 3 / 2 * np.pi) + assert math.isclose(segment_ccw.arc_length, 9 / 2 * np.pi) + + # check exceptions ------------------------------------ + # radius differs + with pytest.raises(Exception): + geo.ArcSegment([3, 3], [6, 10], [6, 3], False) + # radius is zero + with pytest.raises(Exception): + geo.ArcSegment([3, 3], [3, 3], [3, 3], False) + # arc length zero + with pytest.raises(Exception): + geo.ArcSegment([3, 3], [3, 3], [6, 3], False) + with pytest.raises(Exception): + geo.ArcSegment([3, 3], [3, 3], [6, 3], True) + + # test Shape2d ---------------------------------------------------------------- def test_shape2d_construction(): From 7045bd781056a85bf4ece0185643181a6f16a00e Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Wed, 22 Jan 2020 17:13:22 +0100 Subject: [PATCH 074/177] Test new arc segment rasterization --- mypackage/geometry.py | 40 ++++++- tests/test_geometry.py | 236 +++++++++++++++++++++++++++++++---------- 2 files changed, 220 insertions(+), 56 deletions(-) diff --git a/mypackage/geometry.py b/mypackage/geometry.py index d6c8236..aef43ab 100644 --- a/mypackage/geometry.py +++ b/mypackage/geometry.py @@ -219,8 +219,6 @@ def _check_valid(self): if not math.isclose(vec_end_center - vec_start_center, 0): raise ValueError("Radius is not constant.") - if math.isclose(self._radius, 0): - raise Exception("Radius is 0.") if math.isclose(self._arc_length, 0): raise Exception("Arc length is 0.") @@ -259,6 +257,44 @@ def is_arc_winding_ccw(self): """ return self._sign_arc_winding > 0 + def rasterize(self, raster_width, num_points_excluded_end=0): + """ + Create an array of points that describe the segments contour. + + The effective raster width may vary from the specified one, + since the algorithm enforces constant distances between two + raster points. + + :param raster_width: The desired distance between two raster points + :param num_points_excluded_end: Specifies how many points from the + end should be excluded from the rasterization. The main purpose of + this parameter is to avoid point duplication when rasterizing + multiple segments. + :return: Array of contour points + """ + point_start = self._points[:, 0] + point_center = self._points[:, 2] + vec_center_start = (point_start - point_center) + + raster_width = np.clip(raster_width, 0, self.arc_length) + if not raster_width > 0: + raise ValueError("'raster_width' is 0") + + num_raster_segments = int(np.round(self._arc_length / raster_width)) + delta_angle = self._arc_angle / num_raster_segments + + range_modifier = (0.5 - np.floor( + np.abs(num_points_excluded_end))) * delta_angle + max_angle = self._sign_arc_winding * (self._arc_angle + range_modifier) + + angles = np.arange(0, max_angle, self._sign_arc_winding * delta_angle) + + rotation_matrices = R.from_euler('z', angles).as_dcm()[:, 0:2, 0:2] + + data = np.matmul(rotation_matrices, vec_center_start) + point_center + + return data.transpose() + class Shape2D: """Defines a shape in 2 dimensions.""" diff --git a/tests/test_geometry.py b/tests/test_geometry.py index 7e8d456..5347d05 100644 --- a/tests/test_geometry.py +++ b/tests/test_geometry.py @@ -66,6 +66,54 @@ def test_reflection_multiplier(): geo.reflection_multiplier([[2, 2], [1, 1]]) +# helper for segment tests ---------------------------------------------------- + +def default_segment_rasterization_tests(segment, raster_width, point_start, + point_end): + data = segment.rasterize(raster_width) + + # check dimensions are correct + assert len(data.shape) == 2 + + point_dimension = data.shape[0] + num_points = data.shape[1] + assert point_dimension == 2 + + # Check if first and last point of the data are identical to the segment + # start and end + helper.check_vectors_identical(data[:, 0], point_start) + helper.check_vectors_identical(data[:, -1], point_end) + + for i in range(num_points - 1): + point = data[:, i] + next_point = data[:, i + 1] + + raster_width_eff = np.linalg.norm(next_point - point) + assert np.abs(raster_width_eff - raster_width) < 0.1 * raster_width + + # check rasterization with excluded points + data_m2 = segment.rasterize(raster_width, 2) + + num_points_m2 = data_m2.shape[1] + + assert num_points - 2 == num_points_m2 + + for i in range(num_points_m2): + helper.check_vectors_identical(data[:, i], data_m2[:, i]) + + # check that rasterization with to large raster width still works + data_200 = segment.rasterize(200) + + num_points_200 = data_200.shape[1] + assert num_points_200 == 2 + helper.check_vectors_identical(point_start, data_200[:, 0]) + helper.check_vectors_identical(point_end, data_200[:, 1]) + + # check exceptions + with pytest.raises(ValueError): + segment.rasterize(0) + + # test LineSegment ------------------------------------------------------------ def test_line_segment(): @@ -77,60 +125,64 @@ def test_line_segment(): def test_line_segment_rasterization(): + raster_width = 0.1 + point_start = np.array([3, 3]) point_end = np.array([4, 5]) vec_start_end = point_end - point_start unit_vec_start_end = vec_start_end / np.linalg.norm(vec_start_end) segment = geo.LineSegment(point_start, point_end) - raster_data = segment.rasterize(0.1) - shape = raster_data.shape - assert len(shape) == 2 + # perform default tests + default_segment_rasterization_tests(segment, raster_width, point_start, + point_end) - point_size = shape[0] - num_points = shape[1] - assert point_size == 2 - - for i in range(num_points): + # check that points lie between start and end + raster_data = segment.rasterize(raster_width) + num_points = raster_data.shape[1] + for i in np.arange(1, num_points - 1, 1): point = raster_data[:, i] - if i == 0: - helper.check_vectors_identical(point, point_start) - elif i == num_points - 1: - helper.check_vectors_identical(point, point_end) - else: - vec_start_point = point - point_start - unit_vec_start_point = vec_start_point / np.linalg.norm( - vec_start_point) + vec_start_point = point - point_start + unit_vec_start_point = vec_start_point / np.linalg.norm( + vec_start_point) - assert math.isclose( - np.dot(unit_vec_start_point, unit_vec_start_end), - 1) + assert math.isclose(np.dot(unit_vec_start_point, unit_vec_start_end), + 1) - # check rasterization with excluded points - raster_data_m2 = segment.rasterize(0.1, 2) - num_points_m2 = raster_data_m2.shape[1] +# test ArcSegment ------------------------------------------------------------ - assert num_points - 2 == num_points_m2 - for i in range(num_points_m2): - helper.check_vectors_identical(raster_data[:, i], raster_data_m2[:, i]) - # check that rasterization with to large raster width still works - raster_data_200 = segment.rasterize(200) +def arc_segment_test(point_center, point_start, point_end, raster_width, + arc_winding_ccw, check_winding): + point_center = np.array(point_center) + point_start = np.array(point_start) + point_end = np.array(point_end) - num_points_200 = raster_data_200.shape[1] - assert num_points_200 == 2 - helper.check_vectors_identical(point_start, raster_data_200[:, 0]) - helper.check_vectors_identical(point_end, raster_data_200[:, 1]) + radius_arc = np.linalg.norm(point_start - point_center) - # check exceptions - with pytest.raises(ValueError): - segment.rasterize(0) + arc_segment = geo.ArcSegment(point_start, point_end, point_center, + arc_winding_ccw=arc_winding_ccw) + + # Perform standard segment rasterization tests + default_segment_rasterization_tests(arc_segment, raster_width, point_start, + point_end) + data = arc_segment.rasterize(raster_width) + + num_points = data.shape[1] + for i in range(num_points): + point = data[:, i] + + # Check that winding is correct + assert (check_winding(point, point_center)) + + # Check that points have the correct distance to the arcs center + distance_center_point = np.linalg.norm(point - point_center) + assert math.isclose(distance_center_point, radius_arc, abs_tol=1E-6) -# test ArcSegment ------------------------------------------------------------ def test_arc_segment_construction(): segment_cw = geo.ArcSegment([3, 3], [6, 6], [6, 3], False) @@ -162,6 +214,75 @@ def test_arc_segment_construction(): geo.ArcSegment([3, 3], [3, 3], [6, 3], True) +def test_arc_segment_rasterization(): + # center right of segment line + # ---------------------------- + + point_center = [3, 2] + point_start = [1, 2] + point_end = [3, 4] + raster_width = 0.2 + + def in_second_quadrant(p, c): + return p[0] - 1E-9 <= c[0] and p[1] >= c[1] - 1E-9 + + def not_in_second_quadrant(p, c): + return not (p[0] + 1E-9 < c[0] and p[1] > c[1] + 1E-9) + + arc_segment_test(point_center, point_start, point_end, raster_width, False, + in_second_quadrant) + arc_segment_test(point_center, point_start, point_end, raster_width, True, + not_in_second_quadrant) + + # center left of segment line + # ---------------------------- + + point_center = [-4, -7] + point_start = [-4, -2] + point_end = [-9, -7] + raster_width = 0.1 + + arc_segment_test(point_center, point_start, point_end, raster_width, False, + not_in_second_quadrant) + arc_segment_test(point_center, point_start, point_end, raster_width, True, + in_second_quadrant) + + # center on segment line + # ---------------------- + + point_center = [3, 2] + point_start = [2, 2] + point_end = [4, 2] + raster_width = 0.1 + + def not_below_center(p, c): + return p[1] >= c[1] - 1E-9 + + def not_above_center(p, c): + return p[1] - 1E-9 <= c[1] + + arc_segment_test(point_center, point_start, point_end, raster_width, False, + not_below_center) + arc_segment_test(point_center, point_start, point_end, raster_width, True, + not_above_center) + + # special testcase + # ---------------- + # In a previous version the unit vectors to the start and end point were + # calculated using the norm of the vector to the start, since both + # vector length should be identical (radius). However, floating point + # errors caused the dot product to get greater than 1. In result, + # the angle between both vectors could not be calculated using the arccos. + # This test case will fail in this case. + point_center = [0, 0] + point_start = [-6.6 - 2.8] + point_end = [-6.4 - 4.2] + raster_width = 0.1 + + arc_segment = geo.Shape2D.ArcSegment(point_center) + arc_segment.rasterize(raster_width, point_start, point_end) + + # test Shape2d ---------------------------------------------------------------- def test_shape2d_construction(): @@ -227,7 +348,8 @@ def test_shape2d_with_arc_segment(): shape.add_segment([3, 1], segment=geo.Shape2D.ArcSegment([2.1, 1])) -def default_rasterization_tests(data, raster_width, point_start, point_end): +def default_rasterization_tests_old(data, raster_width, point_start, + point_end): # Check if first point of the data are identical to the segment start assert np.linalg.norm(data[0, 0:2] - point_start) < 1E-9 @@ -249,7 +371,7 @@ def default_rasterization_tests(data, raster_width, point_start, point_end): assert np.abs(raster_width_eff - raster_width) < 0.1 * raster_width -def test_line_segment_rasterizaion(): +def test_line_segment_rasterizaion_old(): point_start = np.array([3, -5]) point_end = np.array([-4, 1]) raster_width = 0.2 @@ -259,7 +381,7 @@ def test_line_segment_rasterizaion(): data = line_segment.rasterize(raster_width, point_start, point_end) # Perform standard segment rasterization tests - default_rasterization_tests(data, raster_width, point_start, point_end) + default_rasterization_tests_old(data, raster_width, point_start, point_end) num_data_points = data[:, 0].size for i in range(num_data_points): @@ -275,8 +397,8 @@ def test_line_segment_rasterizaion(): assert dot_product < np.dot(vec_start_end, vec_start_end) -def arc_segment_test(point_center, point_start, point_end, raster_width, - arc_winding_ccw, check_winding): +def arc_segment_test_old(point_center, point_start, point_end, raster_width, + arc_winding_ccw, check_winding): point_center = np.array(point_center) point_start = np.array(point_start) point_end = np.array(point_end) @@ -290,7 +412,7 @@ def arc_segment_test(point_center, point_start, point_end, raster_width, data = arc_segment.rasterize(raster_width, point_start, point_end) # Perform standard segment rasterization tests - default_rasterization_tests(data, raster_width, point_start, point_end) + default_rasterization_tests_old(data, raster_width, point_start, point_end) num_data_points = data[:, 0].size for i in range(num_data_points): @@ -304,7 +426,7 @@ def arc_segment_test(point_center, point_start, point_end, raster_width, assert np.abs(distance_center_point - radius_arc) < 1E-6 -def test_arc_segment_rasterizaion(): +def test_arc_segment_rasterizaion_old(): # center right of segment line # ---------------------------- @@ -319,10 +441,12 @@ def in_second_quadrant(p, c): def not_in_second_quadrant(p, c): return not (p[0] < c[0] and p[1] > c[1]) - arc_segment_test(point_center, point_start, point_end, raster_width, False, - in_second_quadrant) - arc_segment_test(point_center, point_start, point_end, raster_width, True, - not_in_second_quadrant) + arc_segment_test_old(point_center, point_start, point_end, raster_width, + False, + in_second_quadrant) + arc_segment_test_old(point_center, point_start, point_end, raster_width, + True, + not_in_second_quadrant) # center left of segment line # ---------------------------- @@ -332,10 +456,12 @@ def not_in_second_quadrant(p, c): point_end = [-9, -7] raster_width = 0.1 - arc_segment_test(point_center, point_start, point_end, raster_width, False, - not_in_second_quadrant) - arc_segment_test(point_center, point_start, point_end, raster_width, True, - in_second_quadrant) + arc_segment_test_old(point_center, point_start, point_end, raster_width, + False, + not_in_second_quadrant) + arc_segment_test_old(point_center, point_start, point_end, raster_width, + True, + in_second_quadrant) # center on segment line # ---------------------- @@ -351,10 +477,12 @@ def not_below_center(p, c): def not_above_center(p, c): return p[1] <= c[1] - arc_segment_test(point_center, point_start, point_end, raster_width, False, - not_below_center) - arc_segment_test(point_center, point_start, point_end, raster_width, True, - not_above_center) + arc_segment_test_old(point_center, point_start, point_end, raster_width, + False, + not_below_center) + arc_segment_test_old(point_center, point_start, point_end, raster_width, + True, + not_above_center) # special testcase # ---------------- From 4a2fa11f7f4958fb34b19ab6f5305f66eed41783 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Thu, 23 Jan 2020 10:48:18 +0100 Subject: [PATCH 075/177] Add and test new line segment transformations --- mypackage/geometry.py | 41 ++++++++++++++++++++++++++++++- tests/helpers.py | 2 ++ tests/test_geometry.py | 55 +++++++++++++++++++++++++++++++++++++++++- 3 files changed, 96 insertions(+), 2 deletions(-) diff --git a/mypackage/geometry.py b/mypackage/geometry.py index aef43ab..adeb80f 100644 --- a/mypackage/geometry.py +++ b/mypackage/geometry.py @@ -85,7 +85,7 @@ def reflection_multiplier(transformation_matrix): return np.sign(origin_left_of_line) -# Segments -------------------------------------------------------------------- +# LineSegment ----------------------------------------------------------------- class LineSegment: """Line segment.""" @@ -120,6 +120,34 @@ def length(self): """ return self._length + @property + def point_start(self): + """ + Get the starting point of the segment. + + :return: Starting point + """ + return self._points[:, 0] + + @property + def point_end(self): + """ + Get the end point of the segment. + + :return: End point + """ + return self._points[:, 1] + + def apply_transformation(self, matrix): + """ + Apply a transformation matrix to the segment. + + :param matrix: Transformation matrix + :return: --- + """ + self._points = np.matmul(matrix, self._points) + self._calculate_length() + def rasterize(self, raster_width, num_points_excluded_end=0): """ Create an array of points that describe the segments contour. @@ -152,6 +180,17 @@ def rasterize(self, raster_width, num_points_excluded_end=0): return np.matmul(self._points, weight_matrix) + def translate(self, vector): + """ + Apply a translation to the segment. + + :param vector: Translation vector + :return: --- + """ + self._points += np.ndarray((2, 1), float, np.array(vector, float)) + + +# ArcSegment ------------------------------------------------------------------ class ArcSegment: """Arc segment.""" diff --git a/tests/helpers.py b/tests/helpers.py index e17ef1a..e82066d 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -4,6 +4,8 @@ def check_vectors_identical(a, b, tolerance=1E-9): + a = np.array(a) + b = np.array(b) assert a.size == b.size for i in range(a.size): assert math.isclose(a[i], b[i], abs_tol=tolerance) diff --git a/tests/test_geometry.py b/tests/test_geometry.py index 5347d05..9100632 100644 --- a/tests/test_geometry.py +++ b/tests/test_geometry.py @@ -152,8 +152,61 @@ def test_line_segment_rasterization(): 1) -# test ArcSegment ------------------------------------------------------------ +def test_line_segment_transformations(): + # translation ----------------------------------------- + segment = geo.LineSegment([3, 3], [4, 5]) + segment.translate([-1, 4]) + + helper.check_vectors_identical(segment.point_start, np.array([2, 7])) + helper.check_vectors_identical(segment.point_end, np.array([3, 9])) + assert math.isclose(segment.length, np.sqrt(5)) + + # 45 degree rotation ---------------------------------- + s = np.sin(np.pi / 4.) + c = np.cos(np.pi / 4.) + rotation_matrix = [[c, -s], [s, c]] + + segment = geo.LineSegment([2, 2], [3, 6]) + segment.apply_transformation(rotation_matrix) + + exp_start = [0, np.sqrt(8)] + exp_end = np.matmul(rotation_matrix, [3, 6]) + + helper.check_vectors_identical(segment.point_start, exp_start) + helper.check_vectors_identical(segment.point_end, exp_end) + assert math.isclose(segment.length, np.sqrt(17)) + + # reflection at 45 degree line ------------------------ + v = np.array([-1, 1], dtype=float) + reflection_matrix = np.identity(2) - 2 / np.dot(v, v) * np.outer(v, v) + + segment = geo.LineSegment([-1, 3], [6, 1]) + segment.apply_transformation(reflection_matrix) + helper.check_vectors_identical(segment.point_start, [3, -1]) + helper.check_vectors_identical(segment.point_end, [1, 6]) + assert math.isclose(segment.length, np.sqrt(53)) + + # scaling --------------------------------------------- + scale_matrix = [[4, 0], [0, 0.5]] + + segment = geo.LineSegment([-2, 2], [1, 4]) + segment.apply_transformation(scale_matrix) + + helper.check_vectors_identical(segment.point_start, [-8, 1]) + helper.check_vectors_identical(segment.point_end, [4, 2]) + # length changes due to scaling! + assert math.isclose(segment.length, np.sqrt(145)) + + # exceptions ------------------------------------------ + + # transformation results in length = 0 + zero_matrix = np.zeros((2, 2)) + with pytest.raises(Exception): + segment.apply_transformation(zero_matrix) + + +# test ArcSegment ------------------------------------------------------------ def arc_segment_test(point_center, point_start, point_end, raster_width, arc_winding_ccw, check_winding): From 6e4078c6df9f95314b3772aaa3391ca638e015ef Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Thu, 23 Jan 2020 15:33:48 +0100 Subject: [PATCH 076/177] Add and test new arc segment transformations --- mypackage/geometry.py | 100 ++++++++++++++++++++++++++----- tests/test_geometry.py | 133 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 219 insertions(+), 14 deletions(-) diff --git a/mypackage/geometry.py b/mypackage/geometry.py index adeb80f..4765a00 100644 --- a/mypackage/geometry.py +++ b/mypackage/geometry.py @@ -42,7 +42,7 @@ def vector_points_to_left_of_vector(vector, vector_reference): :param vector_reference: Reference vector :return: 1,-1 or 0 (see description) """ - return np.sign(np.linalg.det([vector_reference, vector])) + return int(np.sign(np.linalg.det([vector_reference, vector]))) def point_left_of_line(point, line_start, line_end): @@ -85,6 +85,23 @@ def reflection_multiplier(transformation_matrix): return np.sign(origin_left_of_line) +def reflection_sign(matrix): + """ + Get a sign indicating if the transformation is a reflection. + + Returns -1 if the transformation contains a reflection and 1 if not. + + :param matrix: Transformation matrix + :return: 1 or -1 (see description) + """ + sign = int(np.sign(np.linalg.det(matrix))) + + if sign == 0: + raise Exception("Invalid transformation") + + return sign + + # LineSegment ----------------------------------------------------------------- class LineSegment: @@ -121,22 +138,22 @@ def length(self): return self._length @property - def point_start(self): + def point_end(self): """ - Get the starting point of the segment. + Get the end point of the segment. - :return: Starting point + :return: End point """ - return self._points[:, 0] + return self._points[:, 1] @property - def point_end(self): + def point_start(self): """ - Get the end point of the segment. + Get the starting point of the segment. - :return: End point + :return: Starting point """ - return self._points[:, 1] + return self._points[:, 0] def apply_transformation(self, matrix): """ @@ -213,11 +230,7 @@ def __init__(self, point_start, point_end, point_center, self._points = np.transpose( np.array([point_start, point_end, point_center], dtype=float)) - self._radius = np.linalg.norm(self._points[:, 0] - self._points[:, 2]) - self._calculate_arc_angle() - self._arc_length = self._arc_angle * self._radius - - self._check_valid() + self._calculate_arc_parameters() def _calculate_arc_angle(self): """ @@ -244,6 +257,18 @@ def _calculate_arc_angle(self): else: self._arc_angle = 2 * np.pi - angle_vecs + def _calculate_arc_parameters(self): + """ + Calculate radius, arc length and arc angle from the segments points. + + :return: --- + """ + self._radius = np.linalg.norm(self._points[:, 0] - self._points[:, 2]) + self._calculate_arc_angle() + self._arc_length = self._arc_angle * self._radius + + self._check_valid() + def _check_valid(self): """ Check if the segments data is valid. @@ -279,6 +304,33 @@ def arc_length(self): """ return self._arc_length + @property + def point_center(self): + """ + Get the center point of the segment. + + :return: Center point + """ + return self._points[:, 2] + + @property + def point_end(self): + """ + Get the end point of the segment. + + :return: End point + """ + return self._points[:, 1] + + @property + def point_start(self): + """ + Get the starting point of the segment. + + :return: Starting point + """ + return self._points[:, 0] + @property def radius(self): """ @@ -288,6 +340,17 @@ def radius(self): """ return self._radius + def apply_transformation(self, matrix): + """ + Apply a transformation to the segment. + + :param matrix: Transformation matrix + :return: --- + """ + self._points = np.matmul(matrix, self._points) + self._sign_arc_winding *= reflection_sign(matrix) + self._calculate_arc_parameters() + def is_arc_winding_ccw(self): """ Get True if the winding order is counter-clockwise. False if clockwise. @@ -334,6 +397,15 @@ def rasterize(self, raster_width, num_points_excluded_end=0): return data.transpose() + def translate(self, vector): + """ + Apply a translation to the segment. + + :param vector: Translation vector + :return: --- + """ + self._points += np.ndarray((2, 1), float, np.array(vector, float)) + class Shape2D: """Defines a shape in 2 dimensions.""" diff --git a/tests/test_geometry.py b/tests/test_geometry.py index 9100632..56e3973 100644 --- a/tests/test_geometry.py +++ b/tests/test_geometry.py @@ -336,6 +336,139 @@ def not_above_center(p, c): arc_segment.rasterize(raster_width, point_start, point_end) +def check_arc_segment_values(segment, point_start, point_end, point_center, + winding_ccw, radius, arc_angle, arc_length): + helper.check_vectors_identical(segment.point_start, np.array(point_start)) + helper.check_vectors_identical(segment.point_end, np.array(point_end)) + helper.check_vectors_identical(segment.point_center, + np.array(point_center)) + + assert segment.is_arc_winding_ccw() is winding_ccw + assert math.isclose(segment.radius, radius) + assert math.isclose(segment.arc_angle, arc_angle) + assert math.isclose(segment.arc_length, arc_length) + + +def test_arc_segment_transformations(): + # translation ----------------------------------------- + segment_cw = geo.ArcSegment([3, 3], [5, 5], [5, 3], False) + segment_ccw = geo.ArcSegment([3, 3], [5, 5], [5, 3], True) + segment_cw.translate([-1, 4]) + segment_ccw.translate([-1, 4]) + + exp_start = [2, 7] + exp_end = [4, 9] + exp_center = [4, 7] + exp_radius = 2 + exp_angle_cw = 0.5 * np.pi + exp_angle_ccw = 1.5 * np.pi + exp_arc_length_cw = np.pi + exp_arc_length_ccw = 3 * np.pi + + check_arc_segment_values(segment_cw, exp_start, exp_end, exp_center, False, + exp_radius, exp_angle_cw, exp_arc_length_cw) + check_arc_segment_values(segment_ccw, exp_start, exp_end, exp_center, True, + exp_radius, exp_angle_ccw, exp_arc_length_ccw) + + # 45 degree rotation ---------------------------------- + s = np.sin(np.pi / 4.) + c = np.cos(np.pi / 4.) + rotation_matrix = [[c, -s], [s, c]] + + segment_cw = geo.ArcSegment([3, 3], [5, 5], [5, 3], False) + segment_ccw = geo.ArcSegment([3, 3], [5, 5], [5, 3], True) + segment_cw.apply_transformation(rotation_matrix) + segment_ccw.apply_transformation(rotation_matrix) + + exp_start = [0, np.sqrt(18)] + exp_end = [0, np.sqrt(50)] + exp_center = np.matmul(rotation_matrix, [5, 3]) + + check_arc_segment_values(segment_cw, exp_start, exp_end, exp_center, False, + exp_radius, exp_angle_cw, exp_arc_length_cw) + check_arc_segment_values(segment_ccw, exp_start, exp_end, exp_center, True, + exp_radius, exp_angle_ccw, exp_arc_length_ccw) + + # reflection at 45 degree line ------------------------ + v = np.array([-1, 1], dtype=float) + reflection_matrix = np.identity(2) - 2 / np.dot(v, v) * np.outer(v, v) + + segment_cw = geo.ArcSegment([3, 2], [5, 4], [5, 2], False) + segment_ccw = geo.ArcSegment([3, 2], [5, 4], [5, 2], True) + segment_cw.apply_transformation(reflection_matrix) + segment_ccw.apply_transformation(reflection_matrix) + + exp_start = [2, 3] + exp_end = [4, 5] + exp_center = [2, 5] + + # Reflection must change winding! + check_arc_segment_values(segment_cw, exp_start, exp_end, exp_center, True, + exp_radius, exp_angle_cw, exp_arc_length_cw) + check_arc_segment_values(segment_ccw, exp_start, exp_end, exp_center, + False, exp_radius, exp_angle_ccw, + exp_arc_length_ccw) + + # scaling both coordinates equally -------------------- + scaling_matrix = [[4, 0], [0, 4]] + + segment_cw = geo.ArcSegment([3, 2], [5, 4], [5, 2], False) + segment_ccw = geo.ArcSegment([3, 2], [5, 4], [5, 2], True) + segment_cw.apply_transformation(scaling_matrix) + segment_ccw.apply_transformation(scaling_matrix) + + exp_start = [12, 8] + exp_end = [20, 16] + exp_center = [20, 8] + + # arc_length and radius changed due to scaling! + exp_radius = 8 + exp_arc_length_cw = np.pi * 4 + exp_arc_length_ccw = np.pi * 12 + + check_arc_segment_values(segment_cw, exp_start, exp_end, exp_center, False, + exp_radius, exp_angle_cw, exp_arc_length_cw) + check_arc_segment_values(segment_ccw, exp_start, exp_end, exp_center, True, + exp_radius, exp_angle_ccw, exp_arc_length_ccw) + + # non-uniform scaling which results in a valid arc ---- + scaling_matrix = [[0.25, 0], [0, 2]] + + segment_cw = geo.ArcSegment([8, 4], [32, 4], [20, 2], False) + segment_ccw = geo.ArcSegment([8, 4], [32, 4], [20, 2], True) + segment_cw.apply_transformation(scaling_matrix) + segment_ccw.apply_transformation(scaling_matrix) + + exp_start = [2, 8] + exp_end = [8, 8] + exp_center = [5, 4] + + # angle, arc length and radius changed due to scaling! + exp_radius = 5 + exp_angle_cw = 2 * np.arcsin(3 / 5) + exp_angle_ccw = 2 * np.pi - 2 * np.arcsin(3 / 5) + exp_arc_length_cw = exp_angle_cw * exp_radius + exp_arc_length_ccw = exp_angle_ccw * exp_radius + + check_arc_segment_values(segment_cw, exp_start, exp_end, exp_center, False, + exp_radius, exp_angle_cw, exp_arc_length_cw) + check_arc_segment_values(segment_ccw, exp_start, exp_end, exp_center, True, + exp_radius, exp_angle_ccw, exp_arc_length_ccw) + + # exceptions ------------------------------------------ + + # transformation distorts arc + segment = geo.ArcSegment([3, 2], [5, 4], [5, 2], False) + with pytest.raises(Exception): + segment.apply_transformation(scaling_matrix) + + # transformation results in length = 0 + segment = geo.ArcSegment([3, 2], [5, 4], [5, 2], False) + zero_matrix = np.zeros((2, 2)) + with pytest.raises(Exception): + segment.apply_transformation(zero_matrix) + + # test Shape2d ---------------------------------------------------------------- def test_shape2d_construction(): From 1140942e56e7140b16fa2245f3fb274e7b839f1a Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Thu, 23 Jan 2020 15:39:08 +0100 Subject: [PATCH 077/177] Rename duplicate test function --- tests/test_geometry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_geometry.py b/tests/test_geometry.py index 56e3973..82e8dd7 100644 --- a/tests/test_geometry.py +++ b/tests/test_geometry.py @@ -710,7 +710,7 @@ def test_shape2d_rasterization(): assert np.abs(raster_width_eff - raster_width) < 0.1 * raster_width -def test_arc_segment_transformations(): +def test_arc_segment_transformations_old(): # create arc segment point_center = [2, 3] segment = geo.Shape2D.ArcSegment(point_center) From 391f2f13fed483cdfbf374b0accedd62a7278914 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Thu, 23 Jan 2020 15:51:27 +0100 Subject: [PATCH 078/177] Test modification to fix linux CI errors --- mypackage/geometry.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mypackage/geometry.py b/mypackage/geometry.py index 4765a00..9c6973c 100644 --- a/mypackage/geometry.py +++ b/mypackage/geometry.py @@ -347,6 +347,7 @@ def apply_transformation(self, matrix): :param matrix: Transformation matrix :return: --- """ + matrix = np.array(matrix, float) self._points = np.matmul(matrix, self._points) self._sign_arc_winding *= reflection_sign(matrix) self._calculate_arc_parameters() From f7a24e9b31d8294e23e78bed3b239abcb527feb7 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Thu, 23 Jan 2020 15:55:31 +0100 Subject: [PATCH 079/177] Print CI failure data --- mypackage/geometry.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/mypackage/geometry.py b/mypackage/geometry.py index 9c6973c..9fb91b9 100644 --- a/mypackage/geometry.py +++ b/mypackage/geometry.py @@ -278,10 +278,15 @@ def _check_valid(self): point_end = self._points[:, 1] point_center = self._points[:, 2] - vec_start_center = np.linalg.norm(point_start - point_center) - vec_end_center = np.linalg.norm(point_end - point_center) - - if not math.isclose(vec_end_center - vec_start_center, 0): + radius_start_center = np.linalg.norm(point_start - point_center) + radius_end_center = np.linalg.norm(point_end - point_center) + + if not math.isclose(radius_end_center - radius_start_center, 0): + print(radius_start_center) + print(radius_end_center) + print(point_start) + print(point_end) + print(point_center) raise ValueError("Radius is not constant.") if math.isclose(self._arc_length, 0): raise Exception("Arc length is 0.") @@ -347,7 +352,6 @@ def apply_transformation(self, matrix): :param matrix: Transformation matrix :return: --- """ - matrix = np.array(matrix, float) self._points = np.matmul(matrix, self._points) self._sign_arc_winding *= reflection_sign(matrix) self._calculate_arc_parameters() From d9fa4e2bee054066daccf4a5fd2f3b013f35742b Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Thu, 23 Jan 2020 16:01:22 +0100 Subject: [PATCH 080/177] Test fix --- mypackage/geometry.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mypackage/geometry.py b/mypackage/geometry.py index 9fb91b9..7e2e9bd 100644 --- a/mypackage/geometry.py +++ b/mypackage/geometry.py @@ -281,7 +281,8 @@ def _check_valid(self): radius_start_center = np.linalg.norm(point_start - point_center) radius_end_center = np.linalg.norm(point_end - point_center) - if not math.isclose(radius_end_center - radius_start_center, 0): + if not math.isclose(radius_end_center - radius_start_center, 0, + abs_tol=1E-9): print(radius_start_center) print(radius_end_center) print(point_start) From 4d36db035795be2abe33f993ab18a77834838cce Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Thu, 23 Jan 2020 16:08:59 +0100 Subject: [PATCH 081/177] Remove output, since error is fixed --- mypackage/geometry.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/mypackage/geometry.py b/mypackage/geometry.py index 7e2e9bd..7922661 100644 --- a/mypackage/geometry.py +++ b/mypackage/geometry.py @@ -283,11 +283,6 @@ def _check_valid(self): if not math.isclose(radius_end_center - radius_start_center, 0, abs_tol=1E-9): - print(radius_start_center) - print(radius_end_center) - print(point_start) - print(point_end) - print(point_center) raise ValueError("Radius is not constant.") if math.isclose(self._arc_length, 0): raise Exception("Arc length is 0.") From 886cb0472822a04db91e1cbdc8d101104bfcd81d Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Thu, 23 Jan 2020 16:54:24 +0100 Subject: [PATCH 082/177] Apply some minor optimizations to arc segment --- mypackage/geometry.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/mypackage/geometry.py b/mypackage/geometry.py index 7922661..cf66881 100644 --- a/mypackage/geometry.py +++ b/mypackage/geometry.py @@ -238,9 +238,9 @@ def _calculate_arc_angle(self): :return: --- """ - point_start = self._points[:, 0] - point_end = self._points[:, 1] - point_center = self._points[:, 2] + point_start = self.point_start + point_end = self.point_end + point_center = self.point_center # Calculate angle between vectors (always the smaller one) unit_center_start = tf.normalize(point_start - point_center) @@ -274,15 +274,15 @@ def _check_valid(self): Check if the segments data is valid. :return: --- """ - point_start = self._points[:, 0] - point_end = self._points[:, 1] - point_center = self._points[:, 2] + point_start = self.point_start + point_end = self.point_end + point_center = self.point_center radius_start_center = np.linalg.norm(point_start - point_center) radius_end_center = np.linalg.norm(point_end - point_center) + radius_diff = radius_end_center - radius_start_center - if not math.isclose(radius_end_center - radius_start_center, 0, - abs_tol=1E-9): + if not math.isclose(radius_diff, 0, abs_tol=1E-9): raise ValueError("Radius is not constant.") if math.isclose(self._arc_length, 0): raise Exception("Arc length is 0.") @@ -375,8 +375,8 @@ def rasterize(self, raster_width, num_points_excluded_end=0): multiple segments. :return: Array of contour points """ - point_start = self._points[:, 0] - point_center = self._points[:, 2] + point_start = self.point_start + point_center = self.point_center vec_center_start = (point_start - point_center) raster_width = np.clip(raster_width, 0, self.arc_length) From fd149470109592a78a0d84d196657f7b23d057e6 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Thu, 23 Jan 2020 17:45:10 +0100 Subject: [PATCH 083/177] Remove old shape segments (Tests fail) --- mypackage/geometry.py | 342 +---------------------------- mypackage/point_cloud_generator.py | 6 +- tests/test_geometry.py | 198 ++--------------- 3 files changed, 39 insertions(+), 507 deletions(-) diff --git a/mypackage/geometry.py b/mypackage/geometry.py index cf66881..f6cd5f4 100644 --- a/mypackage/geometry.py +++ b/mypackage/geometry.py @@ -416,294 +416,18 @@ class Shape2D: min_segment_length = 1E-6 tolerance_comparison = 1E-6 - # Member classes ---------------------------------------------------------- - - class Segment: - """Base class for segments.""" - - def check_valid(self, point_start, point_end): - """ - Check if the segments data is valid. - - Checks if the segments data is compatible with the passed start - and end points. Raises an Exception if not. - - :param point_start: Starting point of the segment - :param point_end: End point of the segment - :return: --- - """ - - def translate(self, _unused_vector): - """ - Apply a translation to the segment. - - :param _unused_vector: Translation vector - :return: --- - """ - - def apply_transformation(self, _unused_transformation_matrix): - """ - Apply a transformation to the segment. - - :param _unused_transformation_matrix: Transformation matrix - :return: --- - """ - - class LineSegment(Segment): - """Line segment.""" - - @staticmethod - def rasterize(raster_width, point_start, point_end): - """ - Create an array of points that describe the segments contour. - - The effective raster width may vary from the specified one, - since the algorithm enforces constant distances between two - raster points. - - :param raster_width: The desired distance between two raster points - :param point_start: Starting point of the segment - :param point_end: End point of the segment - :return: Array of contour points (3d) - """ - length = np.linalg.norm(point_end - point_start) - - num_raster_segments = np.round(length / raster_width) - nrw = 1. / num_raster_segments - - multiplier = np.arange(0, 1 - nrw / 2, nrw)[np.newaxis].transpose() - - raster_data = np.matmul((1 - multiplier), - point_start[np.newaxis]) + np.matmul( - multiplier, point_end[np.newaxis]) - - return raster_data - - class ArcSegment(Segment): - """Segment of a circle.""" - - def __init__(self, point_center, arc_winding_ccw=True): - """ - Constructor. - - :param point_center: Center point of the arc - :param: arc_winding_ccw: Specifies if the arcs winding order is - counter-clockwise - """ - point_center = np.array(point_center, dtype=float) - check_point_data_valid(point_center) - - self._point_center = point_center - if arc_winding_ccw: - self._sign_arc_winding = 1 - else: - self._sign_arc_winding = -1 - - def _arc_angle_and_length(self, vec_center_start, vec_center_end): - """ - Calculate the arcs angle and the arc length. - - :param vec_center_start: Vector from the arcs center to the - starting point - :param vec_center_end: Vector from the arcs center to the end point - :return: Array containing the arcs angle and arc length - """ - sign_arc_winding = self._sign_arc_winding - radius = np.linalg.norm(vec_center_start) - - # Calculate angle between vectors (always the smaller one) - unit_center_start = vec_center_start / radius - unit_center_end = vec_center_end / np.linalg.norm(vec_center_end) - - dot_unit = np.dot(unit_center_start, unit_center_end) - angle_vecs = np.arccos(np.clip(dot_unit, -1, 1)) - - sign_winding_points = vector_points_to_left_of_vector( - vec_center_end, vec_center_start) - - if np.abs(sign_winding_points + sign_arc_winding) > 0: - arc_angle = angle_vecs - else: - arc_angle = 2 * np.pi - angle_vecs - - arc_length = arc_angle * radius - - return [arc_angle, arc_length] - - def _rotation_angles(self, vec_center_start, vec_center_end, - raster_width): - """ - Calculate the rotation angle of each raster point. - - The angles are referring to the vector to the starting point. - - :param vec_center_start: Vector from the arcs center to the - starting point - :param vec_center_end: Vector from the arcs center to the end point - :param raster_width: Desired raster width - :return: Array containing the rotation angles - """ - sign = self._sign_arc_winding - [angle_arc, arc_length] = self._arc_angle_and_length( - vec_center_start, - vec_center_end) - - num_raster_segments = int(np.round(arc_length / raster_width)) - delta_angle = angle_arc / num_raster_segments - - rotation_angles = np.arange(0, - sign * (angle_arc - 0.5 * delta_angle), - sign * delta_angle) - - return rotation_angles - - def _rasterize(self, vec_center_start, rotation_angles): - """ - Create an array of points that describe the segments contour. - - :param vec_center_start: Vector from the arcs center to the - starting point - :param rotation_angles: Array containing the rotation angles - :return: Array of contour points (3d) - """ - vec_center_start_3d = vec_center_start[np.newaxis, :, np.newaxis] - point_center_3d = self._point_center[:, np.newaxis] - - rotation_matrices = R.from_euler('z', rotation_angles).as_dcm() - rotation_matrices = rotation_matrices[:, 0:2, 0:2] - - raster_data = np.matmul(rotation_matrices, - vec_center_start_3d) + point_center_3d - - return raster_data[:, :, 0] - - def check_valid(self, point_start, point_end): - """ - Check if the segments data is valid. - - Checks if the segments data is compatible with the passed start - and end points. Raises an Exception if not. - - :param point_start: Starting point of the segment - :param point_end: End point of the segment - :return: --- - """ - tolerance = Shape2D.tolerance_comparison - point_center = self._point_center - - dist_start_center = np.linalg.norm(point_start - point_center) - dist_end_center = np.linalg.norm(point_end - point_center) - - if not np.abs(dist_end_center - dist_start_center) <= tolerance: - raise ValueError( - "Segment start and end points are not compatible with " - "given center of the arc.") - - def apply_transformation(self, transformation_matrix): - """ - Apply a transformation to the segment. - - :param transformation_matrix: Transformation matrix - :return: --- - """ - self._point_center = np.matmul(transformation_matrix, - self._point_center) - self._sign_arc_winding *= reflection_multiplier( - transformation_matrix) - - def rasterize(self, raster_width, point_start, point_end): - """ - Create an array of points that describe the segments contour. - - The effective raster width may vary from the specified one, - since the algorithm enforces constant distances between two - raster points. - - :param raster_width: The desired distance between two raster points - :param point_start: Starting point of the segment - :param point_end: End point of the segment - :return: Array of contour points (3d) - """ - point_center = self._point_center - - vec_center_start = point_start - point_center - vec_center_end = point_end - point_center - - rotation_angles = self._rotation_angles(vec_center_start, - vec_center_end, - raster_width) - - return self._rasterize(vec_center_start, rotation_angles) - - def translate(self, vector): - """ - Apply a translation to the segment. - - :param vector: Translation vector - :return: --- - """ - self._point_center += vector - - def __init__(self, point0, point1, segment=LineSegment()): - """ - Construct the shape with an initial segment. - - :param point0: first point - :param point1: second point - :param segment: segment - """ - point0 = np.array(point0) - point1 = np.array(point1) - - Shape2D._check_segment(segment, point0, point1) - - self._points = np.array([point0, point1], dtype=float) - self._segments = [segment] - - # Private methods --------------------------------------------------------- - - @staticmethod - def _check_segment(segment, point_start, point_end): - """ - Check if segment is valid. - - :param segment: segment - :param point_start: Starting point of the segment - :param point_end: End point of the segment - :return: --- + def __init__(self, segments): """ - check_point_data_valid(point_start) - check_point_data_valid(point_end) - Shape2D._check_segment_length_valid(point_start, point_end) - Shape2D._check_segment_type_valid(segment) - segment.check_valid(point_start, point_end) - - @staticmethod - def _check_segment_length_valid(point_start, point_end): - """ - Check if a segment length is valid. - - :param point_start: Starting point of the segment - :param point_end: End point of the segment - :return: --- - """ - diff = point_start - point_end - if not np.linalg.norm(diff) >= Shape2D.min_segment_length: - raise Exception("Segment length is too small.") + Constructor. - @staticmethod - def _check_segment_type_valid(segment): + :param segments: Single segment or list of segments """ - Check if the segment type is valid. - :return: --- - """ - if not isinstance(segment, Shape2D.Segment): - raise TypeError("Invalid segment type") + self._segments = [segments] # Public methods ---------------------------------------------------------- - def add_segment(self, point, segment=LineSegment()): + def add_segment(self, segment): """ Add a new segment which is connected to previous one. @@ -711,14 +435,6 @@ def add_segment(self, point, segment=LineSegment()): :param segment: segment :return: --- """ - point = np.array(point) - - Shape2D._check_segment(segment, self._points[-1], point) - - if self.is_shape_closed(): - raise ValueError("Shape is already closed") - - self._points = np.vstack((self._points, point)) self._segments.append(segment) def apply_transformation(self, transformation_matrix): @@ -728,9 +444,6 @@ def apply_transformation(self, transformation_matrix): :param transformation_matrix: Transformation matrix :return: --- """ - self._points = np.matmul(self._points, - np.transpose(transformation_matrix)) - for i in range(self.num_segments()): self._segments[i].apply_transformation(transformation_matrix) @@ -761,28 +474,9 @@ def translate(self, vector): :param vector: Translation vector :return: --- """ - self._points += vector - for i in range(self.num_segments()): self._segments[i].translate(vector) - def is_point_included(self, point): - """ - Check if a point is already part of the shape. - - :param point: Point which should be checked - :return: True or False - """ - return is_row_in_array(point, self._points) - - def is_shape_closed(self): - """ - Check if the shape is already closed. - - :return: True or False - """ - return is_row_in_array(self._points[-1, :], self._points[:-1]) - def num_segments(self): """ Get the number of segments of the shape. @@ -791,22 +485,10 @@ def num_segments(self): """ return len(self._segments) - def num_points(self): - """ - Get the number of points of the shape. - - :return: number of points - """ - return self._points[:, 0].size - @property def segments(self): return self._segments - @property - def points(self): - return self._points - def rasterize(self, raster_width): """ Create an array of points that describe the shapes contour. @@ -818,16 +500,14 @@ def rasterize(self, raster_width): :param raster_width: The desired distance between two raster points :return: Array of contour points (3d) """ - points = self._points + segments = self._segments - raster_data = np.empty([0, 2]) + raster_data = np.empty([2, 0]) for i in range(self.num_segments()): - raster_data = np.vstack((raster_data, - segments[i].rasterize(raster_width, - points[i], - points[i + 1]))) + segment_data = segments[i].rasterize(raster_width, 1) + raster_data = np.hstack((raster_data, segment_data)) - raster_data = np.vstack( - (raster_data, self._points[-1])) + last_point = segments[-1].point_end[:, np.newaxis] + raster_data = np.hstack((raster_data, last_point)) return raster_data diff --git a/mypackage/point_cloud_generator.py b/mypackage/point_cloud_generator.py index 78a14ab..3a3b41e 100644 --- a/mypackage/point_cloud_generator.py +++ b/mypackage/point_cloud_generator.py @@ -61,9 +61,11 @@ def rasterize(self, raster_width): :param: raster_width: Raster width :return: Raster data """ - raster_data = np.empty([0, 2]) + raster_data = np.empty([2, 0]) for shape in self._shapes: - raster_data = np.vstack( + shape_data = shape.rasterize(raster_width) + raster_data.shape + raster_data = np.hstack( (raster_data, shape.rasterize(raster_width))) return raster_data diff --git a/tests/test_geometry.py b/tests/test_geometry.py index 82e8dd7..277e301 100644 --- a/tests/test_geometry.py +++ b/tests/test_geometry.py @@ -39,31 +39,34 @@ def test_point_left_of_line(): assert geo.point_left_of_line(line_start, line_start, line_end) == 0 -def test_reflection_multiplier(): - assert geo.reflection_multiplier([[-1, 0], [0, 1]]) == -1 - assert geo.reflection_multiplier([[1, 0], [0, -1]]) == -1 - assert geo.reflection_multiplier([[0, 1], [1, 0]]) == -1 - assert geo.reflection_multiplier([[0, -1], [-1, 0]]) == -1 - assert geo.reflection_multiplier([[-4, 0], [0, 2]]) == -1 - assert geo.reflection_multiplier([[6, 0], [0, -4]]) == -1 - assert geo.reflection_multiplier([[0, 3], [8, 0]]) == -1 - assert geo.reflection_multiplier([[0, -3], [-2, 0]]) == -1 - - assert geo.reflection_multiplier([[1, 0], [0, 1]]) == 1 - assert geo.reflection_multiplier([[-1, 0], [0, -1]]) == 1 - assert geo.reflection_multiplier([[0, -1], [1, 0]]) == 1 - assert geo.reflection_multiplier([[0, 1], [-1, 0]]) == 1 - assert geo.reflection_multiplier([[5, 0], [0, 6]]) == 1 - assert geo.reflection_multiplier([[-3, 0], [0, -7]]) == 1 - assert geo.reflection_multiplier([[0, -8], [9, 0]]) == 1 - assert geo.reflection_multiplier([[0, 3], [-2, 0]]) == 1 +def test_reflection_sign(): + assert geo.reflection_sign([[-1, 0], [0, 1]]) == -1 + assert geo.reflection_sign([[1, 0], [0, -1]]) == -1 + assert geo.reflection_sign([[0, 1], [1, 0]]) == -1 + assert geo.reflection_sign([[0, -1], [-1, 0]]) == -1 + assert geo.reflection_sign([[-4, 0], [0, 2]]) == -1 + assert geo.reflection_sign([[6, 0], [0, -4]]) == -1 + assert geo.reflection_sign([[0, 3], [8, 0]]) == -1 + assert geo.reflection_sign([[0, -3], [-2, 0]]) == -1 + + assert geo.reflection_sign([[1, 0], [0, 1]]) == 1 + assert geo.reflection_sign([[-1, 0], [0, -1]]) == 1 + assert geo.reflection_sign([[0, -1], [1, 0]]) == 1 + assert geo.reflection_sign([[0, 1], [-1, 0]]) == 1 + assert geo.reflection_sign([[5, 0], [0, 6]]) == 1 + assert geo.reflection_sign([[-3, 0], [0, -7]]) == 1 + assert geo.reflection_sign([[0, -8], [9, 0]]) == 1 + assert geo.reflection_sign([[0, 3], [-2, 0]]) == 1 with pytest.raises(Exception): - geo.reflection_multiplier([[0, 0], [0, 0]]) + geo.reflection_sign([[0, 0], [0, 0]]) with pytest.raises(Exception): - geo.reflection_multiplier([[1, 0], [0, 0]]) + geo.reflection_sign([[1, 0], [0, 0]]) with pytest.raises(Exception): - geo.reflection_multiplier([[2, 2], [1, 1]]) + geo.reflection_sign([[2, 2], [1, 1]]) + + +test_reflection_sign() # helper for segment tests ---------------------------------------------------- @@ -534,159 +537,6 @@ def test_shape2d_with_arc_segment(): shape.add_segment([3, 1], segment=geo.Shape2D.ArcSegment([2.1, 1])) -def default_rasterization_tests_old(data, raster_width, point_start, - point_end): - # Check if first point of the data are identical to the segment start - assert np.linalg.norm(data[0, 0:2] - point_start) < 1E-9 - - point_dimension = data[0, :].size - num_data_points = data[:, 0].size - - assert point_dimension == 2 - - for i in range(num_data_points): - point = data[i] - - # Check if the raster width is close to the specified value - if i < num_data_points - 1: - next_point = data[i + 1, 0:2] - else: - next_point = point_end - - raster_width_eff = np.linalg.norm(next_point - point[0:2]) - assert np.abs(raster_width_eff - raster_width) < 0.1 * raster_width - - -def test_line_segment_rasterizaion_old(): - point_start = np.array([3, -5]) - point_end = np.array([-4, 1]) - raster_width = 0.2 - vec_start_end = point_end - point_start - - line_segment = geo.Shape2D.LineSegment() - data = line_segment.rasterize(raster_width, point_start, point_end) - - # Perform standard segment rasterization tests - default_rasterization_tests_old(data, raster_width, point_start, point_end) - - num_data_points = data[:, 0].size - for i in range(num_data_points): - point = data[i] - - # Check if point is on line - vec_start_point = point[0:2] - point_start - assert np.abs(np.linalg.det([vec_start_end, vec_start_point])) < 1E-6 - - # Check if point lies between start and end - dot_product = np.dot(vec_start_point, vec_start_end) - assert dot_product >= 0 - assert dot_product < np.dot(vec_start_end, vec_start_end) - - -def arc_segment_test_old(point_center, point_start, point_end, raster_width, - arc_winding_ccw, check_winding): - point_center = np.array(point_center) - point_start = np.array(point_start) - point_end = np.array(point_end) - - radius_arc = np.linalg.norm(point_start - point_center) - - arc_segment = geo.Shape2D.ArcSegment(point_center, - arc_winding_ccw=arc_winding_ccw) - arc_segment.check_valid(point_start, point_end) - - data = arc_segment.rasterize(raster_width, point_start, point_end) - - # Perform standard segment rasterization tests - default_rasterization_tests_old(data, raster_width, point_start, point_end) - - num_data_points = data[:, 0].size - for i in range(num_data_points): - point = data[i] - - # Check if points are not rasterized clockwise - assert (check_winding(point[0:2], point_center)) - - # Check that points have the correct distance to the arcs center - distance_center_point = np.linalg.norm(point[0:2] - point_center) - assert np.abs(distance_center_point - radius_arc) < 1E-6 - - -def test_arc_segment_rasterizaion_old(): - # center right of segment line - # ---------------------------- - - point_center = [3, 2] - point_start = [1, 2] - point_end = [3, 4] - raster_width = 0.2 - - def in_second_quadrant(p, c): - return p[0] <= c[0] and p[1] >= c[1] - - def not_in_second_quadrant(p, c): - return not (p[0] < c[0] and p[1] > c[1]) - - arc_segment_test_old(point_center, point_start, point_end, raster_width, - False, - in_second_quadrant) - arc_segment_test_old(point_center, point_start, point_end, raster_width, - True, - not_in_second_quadrant) - - # center left of segment line - # ---------------------------- - - point_center = [-4, -7] - point_start = [-4, -2] - point_end = [-9, -7] - raster_width = 0.1 - - arc_segment_test_old(point_center, point_start, point_end, raster_width, - False, - not_in_second_quadrant) - arc_segment_test_old(point_center, point_start, point_end, raster_width, - True, - in_second_quadrant) - - # center on segment line - # ---------------------- - - point_center = [3, 2] - point_start = [2, 2] - point_end = [4, 2] - raster_width = 0.1 - - def not_below_center(p, c): - return p[1] >= c[1] - - def not_above_center(p, c): - return p[1] <= c[1] - - arc_segment_test_old(point_center, point_start, point_end, raster_width, - False, - not_below_center) - arc_segment_test_old(point_center, point_start, point_end, raster_width, - True, - not_above_center) - - # special testcase - # ---------------- - # In a previous version the unit vectors to the start and end point were - # calculated using the norm of the vector to the start, since both - # vector length should be identical (radius). However, floating point - # errors caused the dot product to get greater than 1. In result, - # the angle between both vectors could not be calculated using the arccos. - # This test case will fail in this case. - point_center = [0, 0] - point_start = [-6.6 - 2.8] - point_end = [-6.4 - 4.2] - raster_width = 0.1 - - arc_segment = geo.Shape2D.ArcSegment(point_center) - arc_segment.rasterize(raster_width, point_start, point_end) - - def test_shape2d_rasterization(): points = np.array([[0, 0], [0, 1], From e4a91417c7f8ab44761b27537cbb1f903ed51e69 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Fri, 24 Jan 2020 12:02:31 +0100 Subject: [PATCH 084/177] Modify ctors of segments --- mypackage/geometry.py | 116 ++++++++++++++++-------- tests/test_geometry.py | 198 ++++++++++++++++++++++------------------- 2 files changed, 188 insertions(+), 126 deletions(-) diff --git a/mypackage/geometry.py b/mypackage/geometry.py index f6cd5f4..0d3e479 100644 --- a/mypackage/geometry.py +++ b/mypackage/geometry.py @@ -63,28 +63,6 @@ def point_left_of_line(point, line_start, line_end): vec_line_start_end) -def reflection_multiplier(transformation_matrix): - """ - Get a multiplier indicating if the transformation is a reflection. - - Returns -1 if the transformation contains a reflection and 1 if not. - - :param transformation_matrix: Transformation matrix - :return: 1 or -1 (see description) - """ - points = np.identity(2) - transformed_points = np.matmul(points, - np.transpose(transformation_matrix)) - origin_left_of_line = point_left_of_line(np.array([0, 0]), - transformed_points[0], - transformed_points[1]) - - if origin_left_of_line == 0: - raise Exception("Invalid transformation") - - return np.sign(origin_left_of_line) - - def reflection_sign(matrix): """ Get a sign indicating if the transformation is a reflection. @@ -107,15 +85,20 @@ def reflection_sign(matrix): class LineSegment: """Line segment.""" - def __init__(self, point_start, point_end): + def __init__(self, points): """ Constructor. - :param point_start: Starting point of the segment - :param point_end: End point of the segment + :param points: 2x2 matrix of points. The first column is the + starting point and the second column the end point. """ - self._points = np.transpose( - np.array([point_start, point_end], dtype=float)) + points = np.array(points, float) + if not len(points.shape) == 2: + raise ValueError("'points' must be a 2d array/matrix.") + if not (points.shape[0] == 2 and points.shape[1] == 2): + raise ValueError("'points' is not a 2x2 matrix.") + + self._points = points self._calculate_length() def _calculate_length(self): @@ -128,6 +111,37 @@ def _calculate_length(self): if math.isclose(self._length, 0): raise ValueError("Segment length is 0.") + @classmethod + def construct_from_points(cls, point_start, point_end): + """ + Construct a line segment from two points. + + :param point_start: Starting point of the segment + :param point_end: End point of the segment + :return: Line segment + """ + + points = np.transpose(np.array([point_start, point_end], dtype=float)) + return cls(points) + + @classmethod + def linear_interpolation(cls, a, b, weight): + """ + Interpolate two line segments linearly. + + :param a: First segment + :param b: Second segment + :param weight: Weighting factor in the range [0 .. 1] where 0 is + segment a and 1 is segment b + :return: Interpolated segment + """ + if not isinstance(a, cls) or not isinstance(b, cls): + raise Exception("Parameters a and b must both be line segments.") + + weight = np.clip(weight, 0, 1) + points = (1 - weight) * a.points + weight * b.points + return cls(points) + @property def length(self): """ @@ -155,6 +169,18 @@ def point_start(self): """ return self._points[:, 0] + @property + def points(self): + """ + Get the segments points in form of a 2x2 matrix. + + The first column represents the starting point and the second one + the end point. + + :return: 2x2 matrix containing the segments points + """ + return self._points + def apply_transformation(self, matrix): """ Apply a transformation matrix to the segment. @@ -212,24 +238,27 @@ def translate(self, vector): class ArcSegment: """Arc segment.""" - def __init__(self, point_start, point_end, point_center, - arc_winding_ccw=True): + def __init__(self, points, arc_winding_ccw=True): """ Constructor. - :param point_start: Starting point of the segment - :param point_end: End point of the segment - :param point_center: Center point of the arc + :param points: 2x3 matrix of points. The first column is the + starting point, the second column the end point and the last the + center point. :param: arc_winding_ccw: Specifies if the arcs winding order is counter-clockwise """ + points = np.array(points, float) + if not len(points.shape) == 2: + raise ValueError("'points' must be a 2d array/matrix.") + if not (points.shape[0] == 2 and points.shape[1] == 3): + raise ValueError("'points' is not a 2x3 matrix.") + if arc_winding_ccw: self._sign_arc_winding = 1 else: self._sign_arc_winding = -1 - - self._points = np.transpose( - np.array([point_start, point_end, point_center], dtype=float)) + self._points = points self._calculate_arc_parameters() def _calculate_arc_angle(self): @@ -287,6 +316,23 @@ def _check_valid(self): if math.isclose(self._arc_length, 0): raise Exception("Arc length is 0.") + @classmethod + def construct_from_points(cls, point_start, point_end, point_center, + arc_winding_ccw=True): + """ + Construct an arc segment from three points. + + :param point_start: Starting point of the segment + :param point_end: End point of the segment + :param point_center: Center point of the arc + :param: arc_winding_ccw: Specifies if the arcs winding order is + counter-clockwise + :return: Arc segment + """ + points = np.transpose( + np.array([point_start, point_end, point_center], dtype=float)) + return cls(points, arc_winding_ccw) + @property def arc_angle(self): """ diff --git a/tests/test_geometry.py b/tests/test_geometry.py index 277e301..2b77e3d 100644 --- a/tests/test_geometry.py +++ b/tests/test_geometry.py @@ -119,12 +119,25 @@ def default_segment_rasterization_tests(segment, raster_width, point_start, # test LineSegment ------------------------------------------------------------ -def test_line_segment(): - segment = geo.LineSegment([3, 3], [4, 5]) +def test_line_segment_construction(): + # class constructor ----------------------------------- + segment = geo.LineSegment([[3, 5], [3, 4]]) assert math.isclose(segment.length, np.sqrt(5)) + # exceptions ------------------------------------------ + # length = 0 + with pytest.raises(ValueError): + geo.LineSegment([[0, 0], [1, 1]]) + # not 2x2 + with pytest.raises(ValueError): + geo.LineSegment([[3, 5], [3, 4], [3, 2]]) + # not a 2d array with pytest.raises(ValueError): - geo.LineSegment([0, 1], [0, 1]) + geo.LineSegment([[[3, 5], [3, 4]]]) + + # factories ------------------------------------------- + segment = geo.LineSegment.construct_from_points([3, 3], [4, 5]) + assert math.isclose(segment.length, np.sqrt(5)) def test_line_segment_rasterization(): @@ -132,10 +145,11 @@ def test_line_segment_rasterization(): point_start = np.array([3, 3]) point_end = np.array([4, 5]) + points = np.array([point_start, point_end]).transpose() vec_start_end = point_end - point_start unit_vec_start_end = vec_start_end / np.linalg.norm(vec_start_end) - segment = geo.LineSegment(point_start, point_end) + segment = geo.LineSegment(points) # perform default tests default_segment_rasterization_tests(segment, raster_width, point_start, @@ -157,7 +171,7 @@ def test_line_segment_rasterization(): def test_line_segment_transformations(): # translation ----------------------------------------- - segment = geo.LineSegment([3, 3], [4, 5]) + segment = geo.LineSegment.construct_from_points([3, 3], [4, 5]) segment.translate([-1, 4]) helper.check_vectors_identical(segment.point_start, np.array([2, 7])) @@ -169,7 +183,7 @@ def test_line_segment_transformations(): c = np.cos(np.pi / 4.) rotation_matrix = [[c, -s], [s, c]] - segment = geo.LineSegment([2, 2], [3, 6]) + segment = geo.LineSegment.construct_from_points([2, 2], [3, 6]) segment.apply_transformation(rotation_matrix) exp_start = [0, np.sqrt(8)] @@ -183,7 +197,7 @@ def test_line_segment_transformations(): v = np.array([-1, 1], dtype=float) reflection_matrix = np.identity(2) - 2 / np.dot(v, v) * np.outer(v, v) - segment = geo.LineSegment([-1, 3], [6, 1]) + segment = geo.LineSegment.construct_from_points([-1, 3], [6, 1]) segment.apply_transformation(reflection_matrix) helper.check_vectors_identical(segment.point_start, [3, -1]) @@ -193,7 +207,7 @@ def test_line_segment_transformations(): # scaling --------------------------------------------- scale_matrix = [[4, 0], [0, 0.5]] - segment = geo.LineSegment([-2, 2], [1, 4]) + segment = geo.LineSegment.construct_from_points([-2, 2], [1, 4]) segment.apply_transformation(scale_matrix) helper.check_vectors_identical(segment.point_start, [-8, 1]) @@ -209,6 +223,21 @@ def test_line_segment_transformations(): segment.apply_transformation(zero_matrix) +def test_line_segment_interpolation(): + segment_a = geo.LineSegment.construct_from_points([1, 3], [7, -3]) + segment_b = geo.LineSegment.construct_from_points([5, -5], [-1, 13]) + + for i in range(5): + weight = i / 4 + segment_c = geo.LineSegment.linear_interpolation(segment_a, + segment_b, + weight) + assert math.isclose(segment_c.points[0, 0], 1 + i) + assert math.isclose(segment_c.points[1, 0], 3 - 2 * i) + assert math.isclose(segment_c.points[0, 1], 7 - 2 * i) + assert math.isclose(segment_c.points[1, 1], -3 + 4 * i) + + # test ArcSegment ------------------------------------------------------------ def arc_segment_test(point_center, point_start, point_end, raster_width, @@ -219,8 +248,10 @@ def arc_segment_test(point_center, point_start, point_end, raster_width, radius_arc = np.linalg.norm(point_start - point_center) - arc_segment = geo.ArcSegment(point_start, point_end, point_center, - arc_winding_ccw=arc_winding_ccw) + arc_segment = geo.ArcSegment.construct_from_points(point_start, + point_end, + point_center, + arc_winding_ccw) # Perform standard segment rasterization tests default_segment_rasterization_tests(arc_segment, raster_width, point_start, @@ -241,8 +272,9 @@ def arc_segment_test(point_center, point_start, point_end, raster_width, def test_arc_segment_construction(): - segment_cw = geo.ArcSegment([3, 3], [6, 6], [6, 3], False) - segment_ccw = geo.ArcSegment([3, 3], [6, 6], [6, 3], True) + points = [[3, 6, 6], [3, 6, 3]] + segment_cw = geo.ArcSegment(points, False) + segment_ccw = geo.ArcSegment(points, True) assert not segment_cw.is_arc_winding_ccw() assert segment_ccw.is_arc_winding_ccw() @@ -257,17 +289,52 @@ def test_arc_segment_construction(): assert math.isclose(segment_ccw.arc_length, 9 / 2 * np.pi) # check exceptions ------------------------------------ + # radius differs + points = [[3, 6, 6], [3, 10, 3]] with pytest.raises(Exception): - geo.ArcSegment([3, 3], [6, 10], [6, 3], False) + geo.ArcSegment(points, False) + # radius is zero + points = [[3, 3, 3], [3, 3, 3]] with pytest.raises(Exception): - geo.ArcSegment([3, 3], [3, 3], [3, 3], False) + geo.ArcSegment(points, False) + # arc length zero + points = [[3, 3, 6], [3, 3, 3]] with pytest.raises(Exception): - geo.ArcSegment([3, 3], [3, 3], [6, 3], False) + geo.ArcSegment(points, False) with pytest.raises(Exception): - geo.ArcSegment([3, 3], [3, 3], [6, 3], True) + geo.ArcSegment(points, True) + + # not 2x3 + points = [[3, 3], [3, 3]] + with pytest.raises(ValueError): + geo.ArcSegment(points) + + # not a 2d array + points = [[[3, 3, 6], [3, 3, 3]]] + with pytest.raises(ValueError): + geo.ArcSegment([[[3, 5], [3, 4]]]) + + # factories ------------------------------------------- + + segment_cw = geo.ArcSegment.construct_from_points([3, 3], [6, 6], [6, 3], + False) + segment_ccw = geo.ArcSegment.construct_from_points([3, 3], [6, 6], [6, 3], + True) + + assert not segment_cw.is_arc_winding_ccw() + assert segment_ccw.is_arc_winding_ccw() + + assert math.isclose(segment_cw.radius, 3) + assert math.isclose(segment_ccw.radius, 3) + + assert math.isclose(segment_cw.arc_angle, 1 / 2 * np.pi) + assert math.isclose(segment_ccw.arc_angle, 3 / 2 * np.pi) + + assert math.isclose(segment_cw.arc_length, 3 / 2 * np.pi) + assert math.isclose(segment_ccw.arc_length, 9 / 2 * np.pi) def test_arc_segment_rasterization(): @@ -322,22 +389,6 @@ def not_above_center(p, c): arc_segment_test(point_center, point_start, point_end, raster_width, True, not_above_center) - # special testcase - # ---------------- - # In a previous version the unit vectors to the start and end point were - # calculated using the norm of the vector to the start, since both - # vector length should be identical (radius). However, floating point - # errors caused the dot product to get greater than 1. In result, - # the angle between both vectors could not be calculated using the arccos. - # This test case will fail in this case. - point_center = [0, 0] - point_start = [-6.6 - 2.8] - point_end = [-6.4 - 4.2] - raster_width = 0.1 - - arc_segment = geo.Shape2D.ArcSegment(point_center) - arc_segment.rasterize(raster_width, point_start, point_end) - def check_arc_segment_values(segment, point_start, point_end, point_center, winding_ccw, radius, arc_angle, arc_length): @@ -354,8 +405,10 @@ def check_arc_segment_values(segment, point_start, point_end, point_center, def test_arc_segment_transformations(): # translation ----------------------------------------- - segment_cw = geo.ArcSegment([3, 3], [5, 5], [5, 3], False) - segment_ccw = geo.ArcSegment([3, 3], [5, 5], [5, 3], True) + segment_cw = geo.ArcSegment.construct_from_points([3, 3], [5, 5], [5, 3], + False) + segment_ccw = geo.ArcSegment.construct_from_points([3, 3], [5, 5], [5, 3], + True) segment_cw.translate([-1, 4]) segment_ccw.translate([-1, 4]) @@ -378,8 +431,10 @@ def test_arc_segment_transformations(): c = np.cos(np.pi / 4.) rotation_matrix = [[c, -s], [s, c]] - segment_cw = geo.ArcSegment([3, 3], [5, 5], [5, 3], False) - segment_ccw = geo.ArcSegment([3, 3], [5, 5], [5, 3], True) + segment_cw = geo.ArcSegment.construct_from_points([3, 3], [5, 5], [5, 3], + False) + segment_ccw = geo.ArcSegment.construct_from_points([3, 3], [5, 5], [5, 3], + True) segment_cw.apply_transformation(rotation_matrix) segment_ccw.apply_transformation(rotation_matrix) @@ -396,8 +451,10 @@ def test_arc_segment_transformations(): v = np.array([-1, 1], dtype=float) reflection_matrix = np.identity(2) - 2 / np.dot(v, v) * np.outer(v, v) - segment_cw = geo.ArcSegment([3, 2], [5, 4], [5, 2], False) - segment_ccw = geo.ArcSegment([3, 2], [5, 4], [5, 2], True) + segment_cw = geo.ArcSegment.construct_from_points([3, 2], [5, 4], [5, 2], + False) + segment_ccw = geo.ArcSegment.construct_from_points([3, 2], [5, 4], [5, 2], + True) segment_cw.apply_transformation(reflection_matrix) segment_ccw.apply_transformation(reflection_matrix) @@ -415,8 +472,10 @@ def test_arc_segment_transformations(): # scaling both coordinates equally -------------------- scaling_matrix = [[4, 0], [0, 4]] - segment_cw = geo.ArcSegment([3, 2], [5, 4], [5, 2], False) - segment_ccw = geo.ArcSegment([3, 2], [5, 4], [5, 2], True) + segment_cw = geo.ArcSegment.construct_from_points([3, 2], [5, 4], [5, 2], + False) + segment_ccw = geo.ArcSegment.construct_from_points([3, 2], [5, 4], [5, 2], + True) segment_cw.apply_transformation(scaling_matrix) segment_ccw.apply_transformation(scaling_matrix) @@ -437,8 +496,10 @@ def test_arc_segment_transformations(): # non-uniform scaling which results in a valid arc ---- scaling_matrix = [[0.25, 0], [0, 2]] - segment_cw = geo.ArcSegment([8, 4], [32, 4], [20, 2], False) - segment_ccw = geo.ArcSegment([8, 4], [32, 4], [20, 2], True) + segment_cw = geo.ArcSegment.construct_from_points([8, 4], [32, 4], [20, 2], + False) + segment_ccw = geo.ArcSegment.construct_from_points([8, 4], [32, 4], + [20, 2], True) segment_cw.apply_transformation(scaling_matrix) segment_ccw.apply_transformation(scaling_matrix) @@ -461,12 +522,14 @@ def test_arc_segment_transformations(): # exceptions ------------------------------------------ # transformation distorts arc - segment = geo.ArcSegment([3, 2], [5, 4], [5, 2], False) + segment = geo.ArcSegment.construct_from_points([3, 2], [5, 4], [5, 2], + False) with pytest.raises(Exception): segment.apply_transformation(scaling_matrix) # transformation results in length = 0 - segment = geo.ArcSegment([3, 2], [5, 4], [5, 2], False) + segment = geo.ArcSegment.construct_from_points([3, 2], [5, 4], [5, 2], + False) zero_matrix = np.zeros((2, 2)) with pytest.raises(Exception): segment.apply_transformation(zero_matrix) @@ -560,53 +623,6 @@ def test_shape2d_rasterization(): assert np.abs(raster_width_eff - raster_width) < 0.1 * raster_width -def test_arc_segment_transformations_old(): - # create arc segment - point_center = [2, 3] - segment = geo.Shape2D.ArcSegment(point_center) - - # check transformation with reflection - reflection_matrix = np.array([[0, 1], [1, 0]]) - translation_pre = np.array([-1, 1]) - translation_post = np.array([2, 1]) - segment_copy = copy.deepcopy(segment) - - segment_copy.translate(translation_pre) - assert segment_copy._point_center[0] == 1 - assert segment_copy._point_center[1] == 4 - - segment_copy.apply_transformation(reflection_matrix) - assert segment_copy._point_center[0] == 4 - assert segment_copy._point_center[1] == 1 - # Check that winding order is NOT changed - assert segment_copy._sign_arc_winding == segment._sign_arc_winding * -1 - - segment_copy.translate(translation_post) - # Check if new center point is correct - assert segment_copy._point_center[0] == 6 - assert segment_copy._point_center[1] == 2 - - # check transformation without reflection - rotation_matrix = np.array([[0, 1], [-1, 0]]) - translation_pre = np.array([3, -2]) - translation_post = np.array([-3, -3]) - - segment_copy = copy.deepcopy(segment) - segment_copy.translate(translation_pre) - assert segment_copy._point_center[0] == 5 - assert segment_copy._point_center[1] == 1 - - segment_copy.apply_transformation(rotation_matrix) - assert segment_copy._point_center[0] == 1 - assert segment_copy._point_center[1] == -5 - # Check that winding order is NOT changed - assert segment_copy._sign_arc_winding == segment._sign_arc_winding - - segment_copy.translate(translation_post) - assert segment_copy._point_center[0] == -2 - assert segment_copy._point_center[1] == -8 - - def default_test_shape(): points = np.array([[3, 4], [5, 0], From c909cfaf10943405ac15d697a62efc0b0723c134 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Fri, 24 Jan 2020 17:57:07 +0100 Subject: [PATCH 085/177] Adjust most tests to new segment classes --- mypackage/geometry.py | 65 ++++++-- tests/test_geometry.py | 230 ++++++++++++++++------------ tests/test_point_cloud_generator.py | 34 ++-- 3 files changed, 204 insertions(+), 125 deletions(-) diff --git a/mypackage/geometry.py b/mypackage/geometry.py index 0d3e479..cb72b24 100644 --- a/mypackage/geometry.py +++ b/mypackage/geometry.py @@ -6,6 +6,16 @@ from scipy.spatial.transform import Rotation as R +# Helper functions ------------------------------------------------------------ + +def to_list(var): + if isinstance(var, list): + return var + if var is None: + return [] + return [var] + + def check_point_data_valid(point): """ Check if the data of a point is valid. @@ -18,6 +28,17 @@ def check_point_data_valid(point): "Point data is invalid. Must be an array with 2 values.") +def is_col_in_array(col, array): + """ + Check if a column (1d array) can be found inside of a 2d array. + + :param row: Column that should be checked + :param array: 2d array + :return: True or False + """ + return is_row_in_array(col, np.transpose(array)) + + # https://codereview.stackexchange.com/questions/193835 def is_row_in_array(row, array): """ @@ -136,7 +157,7 @@ def linear_interpolation(cls, a, b, weight): :return: Interpolated segment """ if not isinstance(a, cls) or not isinstance(b, cls): - raise Exception("Parameters a and b must both be line segments.") + raise TypeError("Parameters a and b must both be line segments.") weight = np.clip(weight, 0, 1) points = (1 - weight) * a.points + weight * b.points @@ -333,6 +354,24 @@ def construct_from_points(cls, point_start, point_end, point_center, np.array([point_start, point_end, point_center], dtype=float)) return cls(points, arc_winding_ccw) + @classmethod + def linear_interpolation(cls, a, b, weight): + """ + Interpolate two arc segments linearly. + + :param a: First segment + :param b: Second segment + :param weight: Weighting factor in the range [0 .. 1] where 0 is + segment a and 1 is segment b + :return: Interpolated segment + """ + # implementation ->segment start and end have to be interpolated + # linearly. Otherwise there might occur gaps in interpolated shapes + # at the connecting segment points --- the center point has to be + # determined automatically. 2 ways -> linear angle interpolation or + # linear radius interpolation + raise Exception("Not implemented.") + @property def arc_angle(self): """ @@ -454,6 +493,8 @@ def translate(self, vector): self._points += np.ndarray((2, 1), float, np.array(vector, float)) +# Shape2d class --------------------------------------------------------------- + class Shape2D: """Defines a shape in 2 dimensions.""" @@ -462,26 +503,25 @@ class Shape2D: min_segment_length = 1E-6 tolerance_comparison = 1E-6 - def __init__(self, segments): + def __init__(self, segments=None): """ Constructor. :param segments: Single segment or list of segments """ - self._segments = [segments] + self._segments = to_list(segments) # Public methods ---------------------------------------------------------- - def add_segment(self, segment): + def add_segments(self, segments): """ - Add a new segment which is connected to previous one. + Add segments to the shape - :param point: end point of the new segment - :param segment: segment + :param segments: Single segment or list of segments :return: --- """ - self._segments.append(segment) + self._segments += to_list(segments) def apply_transformation(self, transformation_matrix): """ @@ -502,12 +542,11 @@ def reflect(self, reflection_normal, distance_to_origin=0): origin :return: --- """ - dot_product = np.dot(reflection_normal, reflection_normal) - outer_product = np.outer(reflection_normal, reflection_normal) - householder_matrix = np.identity(2) - 2 * outer_product / dot_product + v = np.array(reflection_normal, float) + dot_product = np.dot(v, v) + householder_matrix = np.identity(2) - 2 / dot_product * np.outer(v, v) - offset = np.array(reflection_normal) / np.sqrt( - dot_product) * distance_to_origin + offset = v / np.sqrt(dot_product) * distance_to_origin self.translate(-offset) self.apply_transformation(householder_matrix) diff --git a/tests/test_geometry.py b/tests/test_geometry.py index 2b77e3d..9db56bb 100644 --- a/tests/test_geometry.py +++ b/tests/test_geometry.py @@ -66,9 +66,6 @@ def test_reflection_sign(): geo.reflection_sign([[2, 2], [1, 1]]) -test_reflection_sign() - - # helper for segment tests ---------------------------------------------------- def default_segment_rasterization_tests(segment, raster_width, point_start, @@ -237,6 +234,17 @@ def test_line_segment_interpolation(): assert math.isclose(segment_c.points[0, 1], 7 - 2 * i) assert math.isclose(segment_c.points[1, 1], -3 + 4 * i) + # exceptions ------------------------------------------ + + # wrong types + arc_segment = geo.ArcSegment.construct_from_points([0, 0], [1, 1], [1, 0]) + with pytest.raises(TypeError): + geo.LineSegment.linear_interpolation(segment_a, arc_segment, weight) + with pytest.raises(TypeError): + geo.LineSegment.linear_interpolation(arc_segment, segment_a, weight) + with pytest.raises(TypeError): + geo.LineSegment.linear_interpolation(arc_segment, arc_segment, weight) + # test ArcSegment ------------------------------------------------------------ @@ -535,69 +543,53 @@ def test_arc_segment_transformations(): segment.apply_transformation(zero_matrix) -# test Shape2d ---------------------------------------------------------------- +def test_arc_segment_interpolation(): + segment_a = geo.ArcSegment.construct_from_points([0, 0], [1, 1], [1, 0]) + segment_b = geo.ArcSegment.construct_from_points([0, 0], [1, 1], [1, 0]) -def test_shape2d_construction(): - # Test Exception: Segment length too small + # not implemented yet with pytest.raises(Exception): - geo.Shape2D([0, 0], [0, 0]) - - # Test Exception: Invalid point format - with pytest.raises(Exception): - geo.Shape2D([0, 0], [1]) - with pytest.raises(Exception): - geo.Shape2D([0, 0, 4], [1, 1]) - - # Test Exception: Invalid shape type - with pytest.raises(Exception): - geo.Shape2D([0, 0], [0, 1], "wrong type") - - # Create shape - geo.Shape2D([0, 0], [0, 1]) + geo.ArcSegment.linear_interpolation(segment_a, segment_b, 1) -def test_shape2d_boolean_functions(): - shape = geo.Shape2D([0, 0], [0, 1]) +test_arc_segment_interpolation() - # Point included or not - assert (shape.is_point_included([0, 1])) - assert (not shape.is_point_included([5, 1])) +# test Shape2d ---------------------------------------------------------------- -def test_shape2d_segment_addition(): - # Create shape and add segments - shape = geo.Shape2D([0, 0], [0, 1]) - shape.add_segment([2, 2]) - shape.add_segment([1, 0]) +def test_shape2d_construction(): + line_segment = geo.LineSegment.construct_from_points([0, 0], [1, 1]) + arc_segment = geo.ArcSegment.construct_from_points([0, 0], [1, 1], [0, 1]) - # Test Exception: Invalid point format - with pytest.raises(Exception): - shape.add_segment(0) - # Test Exception: Invalid shape type - with pytest.raises(Exception): - shape.add_segment([0, 0], "wrong type") + # Empty construction + shape = geo.Shape2D() + assert shape.num_segments() == 0 - # Close segment - shape.add_segment([0, 0]) + # Single element construction shape + shape = geo.Shape2D(line_segment) + assert shape.num_segments() == 1 - # Test Exception: Shape is already closed - with pytest.raises(ValueError): - shape.add_segment([1, 0]) + # Multi segment construction + shape = geo.Shape2D([arc_segment, line_segment]) + assert shape.num_segments() == 2 + assert isinstance(shape.segments[0], geo.ArcSegment) + assert isinstance(shape.segments[1], geo.LineSegment) - # Number of segments has to be one less than the number of points - assert shape.num_segments() == shape.num_points() - 1 +def test_shape2d_segment_addition(): + # Create shape and add segments + line_segment = geo.LineSegment.construct_from_points([0, 0], [1, 1]) + arc_segment = geo.ArcSegment.construct_from_points([0, 0], [1, 1], [0, 1]) -def test_shape2d_with_arc_segment(): - # Invalid center point - with pytest.raises(ValueError): - geo.Shape2D([0, 0], [1, 1], geo.Shape2D.ArcSegment([0, 1.1])) + shape = geo.Shape2D() + shape.add_segments(line_segment) + assert shape.num_segments() == 1 - shape = geo.Shape2D([0, 0], [1, 1], segment=geo.Shape2D.ArcSegment([0, 1])) - shape.add_segment([2, 2], segment=geo.Shape2D.ArcSegment([2, 1])) - # Invalid center point - with pytest.raises(ValueError): - shape.add_segment([3, 1], segment=geo.Shape2D.ArcSegment([2.1, 1])) + shape.add_segments([arc_segment, arc_segment]) + assert shape.num_segments() == 3 + assert isinstance(shape.segments[0], geo.LineSegment) + assert isinstance(shape.segments[1], geo.ArcSegment) + assert isinstance(shape.segments[2], geo.ArcSegment) def test_shape2d_rasterization(): @@ -605,42 +597,38 @@ def test_shape2d_rasterization(): [0, 1], [1, 1], [1, 0]]) + raster_width = 0.2 - shape = geo.Shape2D(points[0], points[1]) - shape.add_segment(points[2]) - shape.add_segment(points[3]) + shape = geo.Shape2D( + geo.LineSegment.construct_from_points(points[0], points[1])) + shape.add_segments( + geo.LineSegment.construct_from_points(points[1], points[2])) + shape.add_segments( + geo.LineSegment.construct_from_points(points[2], points[3])) data = shape.rasterize(raster_width) - # Segment points must be included + # Segment start and end points points must be included for point in points: - assert geo.is_row_in_array(point, data[:, 0:2]) + assert geo.is_col_in_array(point, data) # check effective raster width - for i in range(1, data[:, 0].size): - raster_width_eff = np.linalg.norm(data[i] - data[i - 1]) + for i in range(1, data.shape[1]): + raster_width_eff = np.linalg.norm(data[:, i] - data[:, i - 1]) assert np.abs(raster_width_eff - raster_width) < 0.1 * raster_width def default_test_shape(): - points = np.array([[3, 4], - [5, 0], - [11, 3]]) - point_center = np.array([6, 3]) - # create shape - arc_segment = geo.Shape2D.ArcSegment(point_center) - shape = geo.Shape2D(points[0], points[1], arc_segment) - shape.add_segment(points[2]) - - return shape + arc_segment = geo.ArcSegment.construct_from_points([3, 4], [5, 0], [6, 3]) + line_segment = geo.LineSegment.construct_from_points([5, 0], [11, 3]) + return geo.Shape2D([arc_segment, line_segment]) def test_shape2d_translation(): def check_point(point, point_ref, translation): - assert point[0] - point_ref[0] - translation[0] < 1E-9 - assert point[1] - point_ref[1] - translation[1] < 1E-9 + helper.check_vectors_identical(point - translation, point_ref) translation = [3, 4] @@ -650,15 +638,26 @@ def check_point(point, point_ref, translation): # apply translation shape.translate(translation) - for i in range(shape.num_points()): - check_point(shape._points[i], shape_ref._points[i], translation) - arc_segment = shape._segments[0] arc_segment_ref = shape_ref._segments[0] - check_point(arc_segment._point_center, arc_segment_ref._point_center, + assert arc_segment.is_arc_winding_ccw() == \ + arc_segment_ref.is_arc_winding_ccw() + + check_point(arc_segment.point_start, arc_segment_ref.point_start, + translation) + check_point(arc_segment.point_end, arc_segment_ref.point_end, + translation) + check_point(arc_segment.point_center, arc_segment_ref.point_center, + translation) + + line_segment = shape._segments[1] + line_segment_ref = shape_ref._segments[1] + + check_point(line_segment.point_start, line_segment_ref.point_start, + translation) + check_point(line_segment.point_end, line_segment_ref.point_end, translation) - assert arc_segment._sign_arc_winding == arc_segment_ref._sign_arc_winding def test_shape2d_transformation(): @@ -675,14 +674,23 @@ def check_point_rotation(point, point_ref): # apply transformation shape.apply_transformation(rotation_matrix) - for i in range(shape.num_points()): - check_point_rotation(shape._points[i], shape_ref._points[i]) + arc_segment = shape.segments[0] + arc_segment_ref = shape_ref.segments[0] - arc_segment = shape._segments[0] - arc_segment_ref = shape_ref._segments[0] - check_point_rotation(arc_segment._point_center, - arc_segment_ref._point_center) - assert arc_segment._sign_arc_winding == arc_segment_ref._sign_arc_winding + assert arc_segment.is_arc_winding_ccw() == \ + arc_segment_ref.is_arc_winding_ccw() + + check_point_rotation(arc_segment.point_start, arc_segment_ref.point_start) + check_point_rotation(arc_segment.point_end, arc_segment_ref.point_end) + check_point_rotation(arc_segment.point_center, + arc_segment_ref.point_center) + + line_segment = shape.segments[1] + line_segment_ref = shape_ref.segments[1] + + check_point_rotation(line_segment.point_start, + line_segment_ref.point_start) + check_point_rotation(line_segment.point_end, line_segment_ref.point_end) # with reflection def check_point_reflection(point, point_ref): @@ -696,14 +704,24 @@ def check_point_reflection(point, point_ref): # apply transformation shape.apply_transformation(reflection_matrix) - for i in range(shape.num_points()): - check_point_reflection(shape._points[i], shape_ref._points[i]) + arc_segment = shape.segments[0] + arc_segment_ref = shape_ref.segments[0] - arc_segment = shape._segments[0] - arc_segment_ref = shape_ref._segments[0] - check_point_reflection(arc_segment._point_center, - arc_segment_ref._point_center) - assert arc_segment._sign_arc_winding == -arc_segment_ref._sign_arc_winding + assert arc_segment.is_arc_winding_ccw() != \ + arc_segment_ref.is_arc_winding_ccw() + + check_point_reflection(arc_segment.point_start, + arc_segment_ref.point_start) + check_point_reflection(arc_segment.point_end, arc_segment_ref.point_end) + check_point_reflection(arc_segment.point_center, + arc_segment_ref.point_center) + + line_segment = shape.segments[1] + line_segment_ref = shape_ref.segments[1] + + check_point_reflection(line_segment.point_start, + line_segment_ref.point_start) + check_point_reflection(line_segment.point_end, line_segment_ref.point_end) def check_reflected_point(point, reflected_point, axis_offset, @@ -712,7 +730,8 @@ def check_reflected_point(point, reflected_point, axis_offset, vec_original_reflected = reflected_point - point mid_point = point + 0.5 * vec_original_reflected shifted_mid_point = mid_point - axis_offset - determinant = np.linalg.det([shifted_mid_point, direction_reflection_axis]) + determinant = np.linalg.det( + [shifted_mid_point, direction_reflection_axis]) assert np.abs(determinant) < 1E-8 @@ -728,14 +747,33 @@ def shape2d_reflect_testcase(normal, distance_to_origin): shape_reflected = copy.deepcopy(shape) shape_reflected.reflect(normal, distance_to_origin) + arc_segment = shape.segments[0] + arc_segment_ref = shape_reflected.segments[0] + line_segment = shape.segments[1] + line_segment_ref = shape_reflected.segments[1] + # check reflected points - check_reflected_point(shape._segments[0]._point_center, - shape_reflected._segments[0]._point_center, offset, + check_reflected_point(arc_segment.point_start, + arc_segment_ref.point_start, + offset, + direction_reflection_axis) + check_reflected_point(arc_segment.point_end, + arc_segment_ref.point_end, + offset, + direction_reflection_axis) + check_reflected_point(arc_segment.point_center, + arc_segment_ref.point_center, + offset, direction_reflection_axis) - for i in range(shape.num_points()): - check_reflected_point(shape._points[i], shape_reflected._points[i], - offset, direction_reflection_axis) + check_reflected_point(line_segment.point_start, + line_segment_ref.point_start, + offset, + direction_reflection_axis) + check_reflected_point(line_segment.point_end, + line_segment_ref.point_end, + offset, + direction_reflection_axis) def test_shape2d_reflect(): diff --git a/tests/test_point_cloud_generator.py b/tests/test_point_cloud_generator.py index b96aa9f..70df269 100644 --- a/tests/test_point_cloud_generator.py +++ b/tests/test_point_cloud_generator.py @@ -11,9 +11,11 @@ # Test profile class ---------------------------------------------------------- def test_profile_construction_and_shape_addition(): - shape = geo.Shape2D([0, 0], [1, 0]) - shape.add_segment([2, -1]) - shape.add_segment([0, -1]) + segment0 = geo.LineSegment.construct_from_points([0, 0], [1, 0]) + segment1 = geo.LineSegment.construct_from_points([1, 0], [2, -1]) + segment2 = geo.LineSegment.construct_from_points([2, -1], [0, -1]) + + shape = geo.Shape2D([segment0, segment1, segment2]) # Check invalid types with pytest.raises(TypeError): @@ -60,20 +62,20 @@ def test_profile_construction_and_shape_addition(): for i in range(shape.num_segments()): assert isinstance(segments_profile[i], type(segments[i])) - - points = shape.points - points_profile = shape_profile.points - - assert points.shape == points_profile.shape - for i in range(shape.num_points()): - helpers.check_vectors_identical(points[i], points_profile[i]) + points = segments[i].points + points_profile = segments_profile[i].points + for j in range(2): + helpers.check_vectors_identical(points[:, j], + points_profile[:, j]) def test_profile_rasterization(): raster_width = 0.1 - shape0 = geo.Shape2D([-1, 0], [-raster_width, 0]) - shape1 = geo.Shape2D([0, 0], [1, 0]) - shape2 = geo.Shape2D([1 + raster_width, 0], [2, 0]) + shape0 = geo.Shape2D( + geo.LineSegment.construct_from_points([-1, 0], [-raster_width, 0])) + shape1 = geo.Shape2D(geo.LineSegment.construct_from_points([0, 0], [1, 0])) + shape2 = geo.Shape2D( + geo.LineSegment.construct_from_points([1 + raster_width, 0], [2, 0])) profile = pcg.Profile([shape0, shape1]) profile.add_shapes(shape2) @@ -83,13 +85,13 @@ def test_profile_rasterization(): # check raster data size expected_number_raster_points = int(round(3 / raster_width)) + 1 - assert len(data[:, 0]) == expected_number_raster_points + assert data.shape[1] == expected_number_raster_points # Check that all shapes are rasterized correct for i in range(int(round(3 / raster_width)) + 1): expected_raster_point_x = i * raster_width - 1 - assert data[i, 0] - expected_raster_point_x < 1E-9 - assert data[i, 1] == 0 + assert data[0, i] - expected_raster_point_x < 1E-9 + assert data[1, i] == 0 # Test trace segment classes -------------------------------------------------- From 4757cf0fa4ecb2c7c2171870d43af9d7c18472f4 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Mon, 27 Jan 2020 07:47:45 +0100 Subject: [PATCH 086/177] Fix last test --- tests/test_point_cloud_generator.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_point_cloud_generator.py b/tests/test_point_cloud_generator.py index 70df269..f62765e 100644 --- a/tests/test_point_cloud_generator.py +++ b/tests/test_point_cloud_generator.py @@ -360,14 +360,14 @@ def test_linear_profile_interpolation_sbs(): a_0 = [0, 0] a_1 = [8, 16] a_2 = [16, 0] - shape_a0 = geo.Shape2D(a_0, a_1) - shape_a0 = geo.Shape2D(a_0, a_1) + shape_a01 = geo.Shape2D(geo.LineSegment([a_0, a_1])) + shape_a12 = geo.Shape2D(geo.LineSegment([a_1, a_2])) b_0 = [-4, 8] b_1 = [0, 8] b_2 = [16, -16] - shape_b0 = geo.Shape2D(b_0, b_1) - shape_b0 = geo.Shape2D(b_0, b_1) + shape_b01 = geo.Shape2D(geo.LineSegment([b_0, b_1])) + shape_b12 = geo.Shape2D(geo.LineSegment([b_1, b_2])) # check weight 0 gives a # profile_c = interpolator.interpolate(profile_a, profile_b, 0) From f88bdd6de1c3fad682cbf383093ee3b49000f975 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Mon, 27 Jan 2020 07:53:51 +0100 Subject: [PATCH 087/177] Remove no longer needed function --- mypackage/geometry.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/mypackage/geometry.py b/mypackage/geometry.py index cb72b24..a555cc3 100644 --- a/mypackage/geometry.py +++ b/mypackage/geometry.py @@ -16,18 +16,6 @@ def to_list(var): return [var] -def check_point_data_valid(point): - """ - Check if the data of a point is valid. - - :param point: Point that should be checked - :return: --- - """ - if not (np.ndim(point) == 1 and point.size == 2): - raise Exception( - "Point data is invalid. Must be an array with 2 values.") - - def is_col_in_array(col, array): """ Check if a column (1d array) can be found inside of a 2d array. From 3455f8d3ff542f57f71e9aef2a74d38541ab8bfe Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Mon, 27 Jan 2020 08:50:57 +0100 Subject: [PATCH 088/177] Get everything working again --- mypackage/geometry.py | 31 +++++++------ mypackage/point_cloud_generator.py | 72 +++++++++++++++--------------- 2 files changed, 50 insertions(+), 53 deletions(-) diff --git a/mypackage/geometry.py b/mypackage/geometry.py index a555cc3..929608a 100644 --- a/mypackage/geometry.py +++ b/mypackage/geometry.py @@ -500,7 +500,18 @@ def __init__(self, segments=None): self._segments = to_list(segments) - # Public methods ---------------------------------------------------------- + @property + def num_segments(self): + """ + Get the number of segments of the shape. + + :return: number of segments + """ + return len(self._segments) + + @property + def segments(self): + return self._segments def add_segments(self, segments): """ @@ -518,7 +529,7 @@ def apply_transformation(self, transformation_matrix): :param transformation_matrix: Transformation matrix :return: --- """ - for i in range(self.num_segments()): + for i in range(self.num_segments): self._segments[i].apply_transformation(transformation_matrix) def reflect(self, reflection_normal, distance_to_origin=0): @@ -547,21 +558,9 @@ def translate(self, vector): :param vector: Translation vector :return: --- """ - for i in range(self.num_segments()): + for i in range(self.num_segments): self._segments[i].translate(vector) - def num_segments(self): - """ - Get the number of segments of the shape. - - :return: number of segments - """ - return len(self._segments) - - @property - def segments(self): - return self._segments - def rasterize(self, raster_width): """ Create an array of points that describe the shapes contour. @@ -577,7 +576,7 @@ def rasterize(self, raster_width): segments = self._segments raster_data = np.empty([2, 0]) - for i in range(self.num_segments()): + for i in range(self.num_segments): segment_data = segments[i].rasterize(raster_width, 1) raster_data = np.hstack((raster_data, segment_data)) diff --git a/mypackage/point_cloud_generator.py b/mypackage/point_cloud_generator.py index 3a3b41e..cbe3e37 100644 --- a/mypackage/point_cloud_generator.py +++ b/mypackage/point_cloud_generator.py @@ -30,6 +30,15 @@ def __init__(self, shapes): self._shapes = [] self.add_shapes(shapes) + @property + def num_shapes(self): + """ + Get the number of shapes of the profile. + + :return: Number of shapes + """ + return len(self._shapes) + def add_shapes(self, shapes): """ Add shapes to the profile. @@ -46,14 +55,6 @@ def add_shapes(self, shapes): self._shapes += shapes - def num_shapes(self): - """ - Get the number of shapes of the profile. - - :return: Number of shapes - """ - return len(self._shapes) - def rasterize(self, raster_width): """ Rasterize the profile. @@ -334,32 +335,27 @@ def interpolate(a, b, weight): if not len(a.shapes) == len(b.shapes): raise Exception("Number of profile shapes do not match.") - shapes = [] - for i in range(len(a.shapes)): - points_a = a.shapes[i].points - points_b = b.shapes[i].points - - if points_a[:, 0].size != points_b[:, 0].size: - raise Exception("Number of shape segments do not match.") + shapes_c = [] + for i in range(a.num_shapes): + shape_a = a.shapes[i] + shape_b = b.shapes[i] - points = (1 - weight) * points_a + weight * points_b + if not shape_a.num_segments == shape_b.num_segments: + print(shape_a.num_segments) + print(shape_b.num_segments) + raise Exception("Number of segments differ.") - segments_a = a.shapes[i].segments - segments_b = b.shapes[i].segments + segments_c = [] + for j in range(shape_a.num_segments): + segment_a = shape_a.segments[j] + segment_b = shape_b.segments[j] - for j in range(len(segments_a)): - if not isinstance(segments_a[j], geo.Shape2D.LineSegment): - raise Exception( - "Only line segments are currently supported") + segments_c += [segment_a.linear_interpolation(segment_a, + segment_b, + weight)] + shapes_c += [geo.Shape2D(segments_c)] - if not isinstance(segments_a[j], type(segments_b[j])): - raise Exception("Shape segment types do not match.") - - if j == 0: - shapes += [geo.Shape2D(points[j], points[j + 1])] - else: - shapes[i].add_segment(points[j + 1]) - return Profile(shapes) + return Profile(shapes_c) class Section: @@ -414,22 +410,24 @@ def _interpolated_profile(self, position): def rasterize(self, raster_width): - raster_data = np.empty([0, 3]) + raster_data = np.empty([3, 0]) trace = self.trace global_cs = tf.CartesianCoordinateSystem3d() for position in np.arange(0, trace.length + 0.01, 0.5): profile = self._interpolated_profile(position) profile_raster_data = profile.rasterize(raster_width) - profile_raster_data = np.insert(profile_raster_data, 1, 0, axis=1) + profile_raster_data = np.insert(profile_raster_data, 1, 0, axis=0) trace_cs = trace.local_coordinate_system(position) rotation = tf.change_of_basis_rotation(trace_cs, global_cs) translation = tf.change_of_basis_translation(trace_cs, global_cs) - profile_raster_data = np.matmul(profile_raster_data, - np.transpose(rotation)) - profile_raster_data += translation + profile_raster_data = np.matmul(rotation, + profile_raster_data) + print(profile_raster_data.shape) + print(translation.shape) + profile_raster_data += translation[:, np.newaxis] - raster_data = np.vstack((raster_data, profile_raster_data)) + raster_data = np.hstack((raster_data, profile_raster_data)) - return raster_data + return raster_data.transpose() From 718c4f2e46cd5563c668959694080f9fb074dff1 Mon Sep 17 00:00:00 2001 From: vhirtham Date: Mon, 27 Jan 2020 10:51:04 +0100 Subject: [PATCH 089/177] Add jupyter notebook - shape tutorial --- tutorials/shapes.ipynb | 294 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 294 insertions(+) create mode 100644 tutorials/shapes.ipynb diff --git a/tutorials/shapes.ipynb b/tutorials/shapes.ipynb new file mode 100644 index 0000000..fa261a5 --- /dev/null +++ b/tutorials/shapes.ipynb @@ -0,0 +1,294 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "from mpl_toolkits.mplot3d import Axes3D # noqa: F401 unused import\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "import copy\n", + "\n", + "import mypackage.geometry as geo\n", + "import mypackage.point_cloud_generator as pcg\n", + "import mypackage.transformations as tr\n" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[]" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "segment0 = geo.LineSegment.construct_from_points([0, 0], [1, 0])\n", + "segment0_data = segment0.rasterize(0.1)\n", + "plt.plot(segment0_data[0], segment0_data[1], \"o\")" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[]" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "segment1 = geo.ArcSegment.construct_from_points([1,0], [2,-1], [2, 0])\n", + "segment1_data = segment1.rasterize(0.1)\n", + "plt.plot(segment1_data[0], segment1_data[1], \"o\")" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[]" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "segment2 = geo.LineSegment.construct_from_points([2, -1], [2, -2])\n", + "segment3 = geo.LineSegment.construct_from_points([2, -2], [0, -2])\n", + "\n", + "shape0 = geo.Shape2D([segment0, segment1, segment2, segment3])\n", + "shape0_data = shape0.rasterize(0.1)\n", + "\n", + "plt.plot(shape0_data[0], shape0_data[1],\"x-\")" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[]" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "plt.plot(shape0_data[0], shape0_data[1],\"x-\")\n", + "shape0.translate([-2.5, 0.5])\n", + "shape0_data = shape0.rasterize(0.1)\n", + "plt.plot(shape0_data[0], shape0_data[1],\"rx-\")" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[]" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "shape_r =copy.deepcopy(shape0)\n", + "shape_r_data = shape_r.rasterize(0.1)\n", + "plt.plot(shape_r_data[0], shape_r_data[1],\"x-\")\n", + "shape_r.apply_transformation(tr.rotation_matrix_z(np.pi/2)[0:2,0:2])\n", + "shape_r_data = shape_r.rasterize(0.1)\n", + "plt.plot(shape_r_data[0], shape_r_data[1],\"rx-\")" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[]" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "shape1 = copy.deepcopy(shape0)\n", + "shape1.reflect([1, 0])\n", + "shape1_data = shape1.rasterize(0.1)\n", + "plt.plot(shape0_data[0], shape0_data[1],\"x-\")\n", + "plt.plot(shape1_data[0], shape1_data[1],\"rx-\")" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[]" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "profile = pcg.Profile([shape0, shape1])\n", + "profile_data = profile.rasterize(0.1)\n", + "plt.plot(profile_data[0], profile_data[1],\"x-\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python (test-environment)", + "language": "python", + "name": "test-environment" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.5" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} From b80e910c9778d67a1ba3bc993ff0a1f040040e70 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Mon, 27 Jan 2020 13:33:00 +0100 Subject: [PATCH 090/177] Add and test shape interpolation --- mypackage/geometry.py | 32 ++++++++++++++++++++++++++++++ mypackage/point_cloud_generator.py | 2 -- tests/test_geometry.py | 24 ++++++++++++++++++++++ 3 files changed, 56 insertions(+), 2 deletions(-) diff --git a/mypackage/geometry.py b/mypackage/geometry.py index 929608a..b18cbfc 100644 --- a/mypackage/geometry.py +++ b/mypackage/geometry.py @@ -500,6 +500,38 @@ def __init__(self, segments=None): self._segments = to_list(segments) + @classmethod + def interpolate(cls, a, b, weight, interpolation_schemes): + if not a.num_segments == b.num_segments: + raise Exception("Number of segments differ.") + + segments_c = [] + for i in range(a.num_segments): + segments_c += [interpolation_schemes[i](a.segments[i], + b.segments[i], + weight)] + return cls(segments_c) + + @classmethod + def linear_interpolation(cls, a, b, weight): + """ + Interpolate 2 shapes linearly. + + Each segment is interpolated individually, using the corresponding + linear segment interpolation. + + :param a: First shape + :param b: Second shape + :param weight: Weighting factor in the range [0 .. 1] where 0 is + shape a and 1 is shape b + :return: Interpolated shape + """ + interpolation_schemes = [] + for i in range(a.num_segments): + interpolation_schemes += [a.segments[i].linear_interpolation] + + return cls.interpolate(a, b, weight, interpolation_schemes) + @property def num_segments(self): """ diff --git a/mypackage/point_cloud_generator.py b/mypackage/point_cloud_generator.py index cbe3e37..81f64c8 100644 --- a/mypackage/point_cloud_generator.py +++ b/mypackage/point_cloud_generator.py @@ -341,8 +341,6 @@ def interpolate(a, b, weight): shape_b = b.shapes[i] if not shape_a.num_segments == shape_b.num_segments: - print(shape_a.num_segments) - print(shape_b.num_segments) raise Exception("Number of segments differ.") segments_c = [] diff --git a/tests/test_geometry.py b/tests/test_geometry.py index 9db56bb..3ed5638 100644 --- a/tests/test_geometry.py +++ b/tests/test_geometry.py @@ -784,3 +784,27 @@ def test_shape2d_reflect(): shape2d_reflect_testcase([-7, 2], 4.12) shape2d_reflect_testcase([-7, -2], 4.12) shape2d_reflect_testcase([7, -2], 4.12) + + +def test_shape2d_linear_interpolation(): + segment_a0 = geo.LineSegment.construct_from_points([0, 0], [1, 1]) + segment_a1 = geo.LineSegment.construct_from_points([1, 1], [2, 0]) + shape_a = geo.Shape2D([segment_a0, segment_a1]) + + segment_b0 = geo.LineSegment.construct_from_points([1, 1], [2, -1]) + segment_b1 = geo.LineSegment.construct_from_points([4, -1], [3, 5]) + shape_b = geo.Shape2D([segment_b0, segment_b1]) + + for i in range(5): + weight = i / 4. + shape_c = geo.Shape2D.linear_interpolation(shape_a, shape_b, weight) + + helper.check_vectors_identical(shape_c.segments[0].point_start, + [weight, weight]) + helper.check_vectors_identical(shape_c.segments[0].point_end, + [1 + weight, 1 - 2 * weight]) + + helper.check_vectors_identical(shape_c.segments[1].point_start, + [1 + 3 * weight, 1 - 2 * weight]) + helper.check_vectors_identical(shape_c.segments[1].point_end, + [2 + weight, 5 * weight]) From 850eb2f19a67fb0be8f6b80b03c1db1c1acea10b Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Mon, 27 Jan 2020 13:38:40 +0100 Subject: [PATCH 091/177] Fix tests --- tests/test_geometry.py | 10 +++++----- tests/test_point_cloud_generator.py | 16 ++++++++-------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/tests/test_geometry.py b/tests/test_geometry.py index 3ed5638..a875cb6 100644 --- a/tests/test_geometry.py +++ b/tests/test_geometry.py @@ -563,15 +563,15 @@ def test_shape2d_construction(): # Empty construction shape = geo.Shape2D() - assert shape.num_segments() == 0 + assert shape.num_segments == 0 # Single element construction shape shape = geo.Shape2D(line_segment) - assert shape.num_segments() == 1 + assert shape.num_segments == 1 # Multi segment construction shape = geo.Shape2D([arc_segment, line_segment]) - assert shape.num_segments() == 2 + assert shape.num_segments == 2 assert isinstance(shape.segments[0], geo.ArcSegment) assert isinstance(shape.segments[1], geo.LineSegment) @@ -583,10 +583,10 @@ def test_shape2d_segment_addition(): shape = geo.Shape2D() shape.add_segments(line_segment) - assert shape.num_segments() == 1 + assert shape.num_segments == 1 shape.add_segments([arc_segment, arc_segment]) - assert shape.num_segments() == 3 + assert shape.num_segments == 3 assert isinstance(shape.segments[0], geo.LineSegment) assert isinstance(shape.segments[1], geo.ArcSegment) assert isinstance(shape.segments[2], geo.ArcSegment) diff --git a/tests/test_point_cloud_generator.py b/tests/test_point_cloud_generator.py index f62765e..120e46d 100644 --- a/tests/test_point_cloud_generator.py +++ b/tests/test_point_cloud_generator.py @@ -27,9 +27,9 @@ def test_profile_construction_and_shape_addition(): # Check valid types profile = pcg.Profile(shape) - assert profile.num_shapes() == 1 + assert profile.num_shapes == 1 profile = pcg.Profile([shape, shape]) - assert profile.num_shapes() == 2 + assert profile.num_shapes == 2 # Check invalid addition with pytest.raises(TypeError): @@ -41,26 +41,26 @@ def test_profile_construction_and_shape_addition(): # Check that invalid calls only raise an exception and do not invalidate # the internal data - assert profile.num_shapes() == 2 + assert profile.num_shapes == 2 # Check valid addition profile.add_shapes(shape) - assert profile.num_shapes() == 3 + assert profile.num_shapes == 3 profile.add_shapes([shape, shape]) - assert profile.num_shapes() == 5 + assert profile.num_shapes == 5 # Check shapes shapes_profile = profile.shapes for shape_profile in shapes_profile: - assert shape.num_segments() == shape_profile.num_segments() + assert shape.num_segments == shape_profile.num_segments segments = shape.segments segments_profile = shape_profile.segments - assert len(segments) == shape.num_segments() + assert len(segments) == shape.num_segments assert len(segments) == len(segments_profile) - for i in range(shape.num_segments()): + for i in range(shape.num_segments): assert isinstance(segments_profile[i], type(segments[i])) points = segments[i].points points_profile = segments_profile[i].points From c4949bb06a96ec79fd5c6e3396b5e5de2a3f3dac Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Mon, 27 Jan 2020 13:48:45 +0100 Subject: [PATCH 092/177] Add more shape interpolation testcases --- tests/test_geometry.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/test_geometry.py b/tests/test_geometry.py index a875cb6..5a2dc5e 100644 --- a/tests/test_geometry.py +++ b/tests/test_geometry.py @@ -808,3 +808,32 @@ def test_shape2d_linear_interpolation(): [1 + 3 * weight, 1 - 2 * weight]) helper.check_vectors_identical(shape_c.segments[1].point_end, [2 + weight, 5 * weight]) + + # check weight clipped to valid range ----------------- + + shape_c = geo.Shape2D.linear_interpolation(shape_a, shape_b, -3) + + helper.check_vectors_identical(shape_c.segments[0].point_start, + shape_a.segments[0].point_start) + helper.check_vectors_identical(shape_c.segments[0].point_end, + shape_a.segments[0].point_end) + helper.check_vectors_identical(shape_c.segments[1].point_start, + shape_a.segments[1].point_start) + helper.check_vectors_identical(shape_c.segments[1].point_end, + shape_a.segments[1].point_end) + + shape_c = geo.Shape2D.linear_interpolation(shape_a, shape_b, 100) + + helper.check_vectors_identical(shape_c.segments[0].point_start, + shape_b.segments[0].point_start) + helper.check_vectors_identical(shape_c.segments[0].point_end, + shape_b.segments[0].point_end) + helper.check_vectors_identical(shape_c.segments[1].point_start, + shape_b.segments[1].point_start) + helper.check_vectors_identical(shape_c.segments[1].point_end, + shape_b.segments[1].point_end) + + # exceptions ------------------------------------------ + shape_a.add_segments(segment_a1) + with pytest.raises(Exception): + geo.Shape2D.interpolate(shape_a, shape_b, 0.25) From 53033725ca0e0b2b237a5e9dfad2fd761d40203e Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Mon, 27 Jan 2020 13:53:26 +0100 Subject: [PATCH 093/177] Add testcases for line segment interpolation --- tests/test_geometry.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/test_geometry.py b/tests/test_geometry.py index 5a2dc5e..6f20764 100644 --- a/tests/test_geometry.py +++ b/tests/test_geometry.py @@ -234,6 +234,18 @@ def test_line_segment_interpolation(): assert math.isclose(segment_c.points[0, 1], 7 - 2 * i) assert math.isclose(segment_c.points[1, 1], -3 + 4 * i) + # check weight clipped to valid range ----------------- + + segment_c = geo.LineSegment.linear_interpolation(segment_a, segment_b, -3) + helper.check_vectors_identical(segment_c.point_start, + segment_a.point_start) + helper.check_vectors_identical(segment_c.point_end, segment_a.point_end) + + segment_c = geo.LineSegment.linear_interpolation(segment_a, segment_b, 6) + helper.check_vectors_identical(segment_c.point_start, + segment_b.point_start) + helper.check_vectors_identical(segment_c.point_end, segment_b.point_end) + # exceptions ------------------------------------------ # wrong types From 4f69355535076aef75d505e5e942e8d3d9927eee Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Mon, 27 Jan 2020 14:21:56 +0100 Subject: [PATCH 094/177] Test linear profile interpolation --- mypackage/geometry.py | 4 +-- mypackage/point_cloud_generator.py | 21 +++---------- tests/test_point_cloud_generator.py | 47 +++++++++++++++++++++-------- 3 files changed, 40 insertions(+), 32 deletions(-) diff --git a/mypackage/geometry.py b/mypackage/geometry.py index b18cbfc..ed018b4 100644 --- a/mypackage/geometry.py +++ b/mypackage/geometry.py @@ -486,8 +486,6 @@ def translate(self, vector): class Shape2D: """Defines a shape in 2 dimensions.""" - # Member variables -------------------------------------------------------- - min_segment_length = 1E-6 tolerance_comparison = 1E-6 @@ -505,6 +503,8 @@ def interpolate(cls, a, b, weight, interpolation_schemes): if not a.num_segments == b.num_segments: raise Exception("Number of segments differ.") + weight = np.clip(weight, 0, 1) + segments_c = [] for i in range(a.num_segments): segments_c += [interpolation_schemes[i](a.segments[i], diff --git a/mypackage/point_cloud_generator.py b/mypackage/point_cloud_generator.py index 81f64c8..afa8e7a 100644 --- a/mypackage/point_cloud_generator.py +++ b/mypackage/point_cloud_generator.py @@ -337,21 +337,9 @@ def interpolate(a, b, weight): shapes_c = [] for i in range(a.num_shapes): - shape_a = a.shapes[i] - shape_b = b.shapes[i] - - if not shape_a.num_segments == shape_b.num_segments: - raise Exception("Number of segments differ.") - - segments_c = [] - for j in range(shape_a.num_segments): - segment_a = shape_a.segments[j] - segment_b = shape_b.segments[j] - - segments_c += [segment_a.linear_interpolation(segment_a, - segment_b, - weight)] - shapes_c += [geo.Shape2D(segments_c)] + shapes_c += [geo.Shape2D.linear_interpolation(a.shapes[i], + b.shapes[i], + weight)] return Profile(shapes_c) @@ -422,8 +410,7 @@ def rasterize(self, raster_width): profile_raster_data = np.matmul(rotation, profile_raster_data) - print(profile_raster_data.shape) - print(translation.shape) + profile_raster_data += translation[:, np.newaxis] raster_data = np.hstack((raster_data, profile_raster_data)) diff --git a/tests/test_point_cloud_generator.py b/tests/test_point_cloud_generator.py index 120e46d..aaea986 100644 --- a/tests/test_point_cloud_generator.py +++ b/tests/test_point_cloud_generator.py @@ -354,24 +354,45 @@ def test_trace_local_coordinate_system(): # Profile interpolation classes ----------------------------------------------- -def test_linear_profile_interpolation_sbs(): - interpolator = pcg.LinearProfileInterpolationSBS +def check_interpolated_profile_points(profile, c_0, c_1, c_2): + helpers.check_vectors_identical(profile.shapes[0].segments[0].point_start, + c_0) + helpers.check_vectors_identical(profile.shapes[0].segments[0].point_end, + c_1) + helpers.check_vectors_identical(profile.shapes[1].segments[0].point_start, + c_1) + helpers.check_vectors_identical(profile.shapes[1].segments[0].point_end, + c_2) + +def test_linear_profile_interpolation_sbs(): a_0 = [0, 0] a_1 = [8, 16] a_2 = [16, 0] - shape_a01 = geo.Shape2D(geo.LineSegment([a_0, a_1])) - shape_a12 = geo.Shape2D(geo.LineSegment([a_1, a_2])) + shape_a01 = geo.Shape2D(geo.LineSegment.construct_from_points(a_0, a_1)) + shape_a12 = geo.Shape2D(geo.LineSegment.construct_from_points(a_1, a_2)) + profile_a = pcg.Profile([shape_a01, shape_a12]) b_0 = [-4, 8] b_1 = [0, 8] b_2 = [16, -16] - shape_b01 = geo.Shape2D(geo.LineSegment([b_0, b_1])) - shape_b12 = geo.Shape2D(geo.LineSegment([b_1, b_2])) - - # check weight 0 gives a - # profile_c = interpolator.interpolate(profile_a, profile_b, 0) - - # exp0 = [-1, 2] - # exp1 = [2, 14] - # exp2 = [16, -4] + shape_b01 = geo.Shape2D(geo.LineSegment.construct_from_points(b_0, b_1)) + shape_b12 = geo.Shape2D(geo.LineSegment.construct_from_points(b_1, b_2)) + profile_b = pcg.Profile([shape_b01, shape_b12]) + + for i in range(5): + weight = i / 4. + profile_c = pcg.LinearProfileInterpolationSBS.interpolate(profile_a, + profile_b, + weight) + check_interpolated_profile_points(profile_c, + [-i, 2 * i], + [8 - 2 * i, 16 - 2 * i], + [16, -4 * i]) + + # check weight clipped to valid range + profile_c = pcg.LinearProfileInterpolationSBS.interpolate(profile_a, + profile_b, + -3) + + check_interpolated_profile_points(profile_c, a_0, a_1, a_2) From 302d04ace796241d3ca3cfd3ca2dc94557bfd5df Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Mon, 27 Jan 2020 14:27:46 +0100 Subject: [PATCH 095/177] Fix a testcase which triggered a wrong exception --- tests/test_geometry.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test_geometry.py b/tests/test_geometry.py index 6f20764..a05fcb3 100644 --- a/tests/test_geometry.py +++ b/tests/test_geometry.py @@ -846,6 +846,7 @@ def test_shape2d_linear_interpolation(): shape_b.segments[1].point_end) # exceptions ------------------------------------------ - shape_a.add_segments(segment_a1) + + geo.Shape2D.linear_interpolation(shape_a, shape_b, 0.25) with pytest.raises(Exception): - geo.Shape2D.interpolate(shape_a, shape_b, 0.25) + geo.Shape2D.linear_interpolation(shape_a, shape_b, 0.25) From c086521f5afda91e7a9257de78af96c094dcfb57 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Mon, 27 Jan 2020 14:35:25 +0100 Subject: [PATCH 096/177] Test profile interpolation exceptions --- tests/test_point_cloud_generator.py | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/tests/test_point_cloud_generator.py b/tests/test_point_cloud_generator.py index aaea986..e5f38f6 100644 --- a/tests/test_point_cloud_generator.py +++ b/tests/test_point_cloud_generator.py @@ -390,9 +390,33 @@ def test_linear_profile_interpolation_sbs(): [8 - 2 * i, 16 - 2 * i], [16, -4 * i]) - # check weight clipped to valid range + # check weight clipped to valid range ----------------- + profile_c = pcg.LinearProfileInterpolationSBS.interpolate(profile_a, profile_b, -3) check_interpolated_profile_points(profile_c, a_0, a_1, a_2) + + profile_c = pcg.LinearProfileInterpolationSBS.interpolate(profile_a, + profile_b, + 42) + + check_interpolated_profile_points(profile_c, b_0, b_1, b_2) + + # exceptions ------------------------------------------ + + # number of shapes differ + profile_d = pcg.Profile([shape_b01, shape_b12, shape_a12]) + with pytest.raises(Exception): + pcg.LinearProfileInterpolationSBS.interpolate(profile_d, profile_b, + 0.5) + + # number of segments differ + shape_b012 = geo.Shape2D([geo.LineSegment.construct_from_points(b_0, b_1), + geo.LineSegment.construct_from_points(b_1, b_2)]) + + profile_b2 = pcg.Profile([shape_b01, shape_b012]) + pcg.LinearProfileInterpolationSBS.interpolate(profile_a, + profile_b2, + 0.2) From b4834d358f81aa89914e3c1d96d82a27cf6a19be Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Mon, 27 Jan 2020 14:41:50 +0100 Subject: [PATCH 097/177] Fix tests --- tests/test_geometry.py | 2 +- tests/test_point_cloud_generator.py | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/test_geometry.py b/tests/test_geometry.py index a05fcb3..6666576 100644 --- a/tests/test_geometry.py +++ b/tests/test_geometry.py @@ -847,6 +847,6 @@ def test_shape2d_linear_interpolation(): # exceptions ------------------------------------------ - geo.Shape2D.linear_interpolation(shape_a, shape_b, 0.25) + shape_a.add_segments([segment_a1]) with pytest.raises(Exception): geo.Shape2D.linear_interpolation(shape_a, shape_b, 0.25) diff --git a/tests/test_point_cloud_generator.py b/tests/test_point_cloud_generator.py index e5f38f6..6132415 100644 --- a/tests/test_point_cloud_generator.py +++ b/tests/test_point_cloud_generator.py @@ -417,6 +417,7 @@ def test_linear_profile_interpolation_sbs(): geo.LineSegment.construct_from_points(b_1, b_2)]) profile_b2 = pcg.Profile([shape_b01, shape_b012]) - pcg.LinearProfileInterpolationSBS.interpolate(profile_a, - profile_b2, - 0.2) + with pytest.raises(Exception): + pcg.LinearProfileInterpolationSBS.interpolate(profile_a, + profile_b2, + 0.2) From 57f90199d22846e9b7afea67ab80f4d2534ef31b Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Mon, 27 Jan 2020 14:59:07 +0100 Subject: [PATCH 098/177] Remove unused class members --- mypackage/geometry.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/mypackage/geometry.py b/mypackage/geometry.py index ed018b4..9d813fc 100644 --- a/mypackage/geometry.py +++ b/mypackage/geometry.py @@ -486,9 +486,6 @@ def translate(self, vector): class Shape2D: """Defines a shape in 2 dimensions.""" - min_segment_length = 1E-6 - tolerance_comparison = 1E-6 - def __init__(self, segments=None): """ Constructor. @@ -504,7 +501,7 @@ def interpolate(cls, a, b, weight, interpolation_schemes): raise Exception("Number of segments differ.") weight = np.clip(weight, 0, 1) - + segments_c = [] for i in range(a.num_segments): segments_c += [interpolation_schemes[i](a.segments[i], From 11790e915cf8c7adecf433d516ce11e521ef63ad Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Mon, 27 Jan 2020 16:15:04 +0100 Subject: [PATCH 099/177] Add and test varying profile class --- mypackage/point_cloud_generator.py | 49 ++++++++++++++++++ tests/test_point_cloud_generator.py | 78 +++++++++++++++++++++++++++++ 2 files changed, 127 insertions(+) diff --git a/mypackage/point_cloud_generator.py b/mypackage/point_cloud_generator.py index afa8e7a..d8977b3 100644 --- a/mypackage/point_cloud_generator.py +++ b/mypackage/point_cloud_generator.py @@ -317,6 +317,8 @@ def local_coordinate_system(self, position): return segment_start_cs + local_segment_cs +# Linear profile interpolation class ------------------------------------------ + class LinearProfileInterpolationSBS: """Linear segment by segment interpolation class for profiles.""" @@ -344,6 +346,53 @@ def interpolate(a, b, weight): return Profile(shapes_c) +# Varying profile class ------------------------------------------------------- + +class VaryingProfile: + def __init__(self, profiles, locations, interpolation_schemes): + locations = to_list(locations) + interpolation_schemes = to_list(interpolation_schemes) + + if not locations[0] == 0: + locations = [0] + locations + + if not len(profiles) == len(locations): + raise Exception( + "Invalid list of locations. See function description.") + + if not len(interpolation_schemes) == len(profiles) - 1: + raise Exception( + "Number of interpolations must be 1 less than number of " + "profiles.") + + for i in range(len(profiles) - 1): + if locations[i] >= locations[i + 1]: + raise Exception( + "Locations need to be sorted in ascending order.") + + self._profiles = profiles + self._locations = locations + self._interpolation_schemes = interpolation_schemes + + @property + def locations(self): + return self._locations + + @property + def num_interpolation_schemes(self): + return len(self._interpolation_schemes) + + @property + def num_locations(self): + return len(self._locations) + + @property + def num_profiles(self): + return len(self._profiles) + + +# Section class --------------------------------------------------------------- + class Section: """Defines a section""" diff --git a/tests/test_point_cloud_generator.py b/tests/test_point_cloud_generator.py index 6132415..587028c 100644 --- a/tests/test_point_cloud_generator.py +++ b/tests/test_point_cloud_generator.py @@ -421,3 +421,81 @@ def test_linear_profile_interpolation_sbs(): pcg.LinearProfileInterpolationSBS.interpolate(profile_a, profile_b2, 0.2) + + +# test varying profile -------------------------------------------------------- + +def check_varying_profile_state(varying_profile, locations): + num_profiles = len(locations) + assert varying_profile.num_interpolation_schemes == num_profiles - 1 + assert varying_profile.num_locations == num_profiles + assert varying_profile.num_profiles == num_profiles + + for i in range(num_profiles): + assert math.isclose(locations[i], varying_profile.locations[i]) + + +def test_varying_profile_construction(): + interpol = pcg.LinearProfileInterpolationSBS + + a_0 = [0, 0] + a_1 = [8, 16] + a_2 = [16, 0] + shape_a01 = geo.Shape2D(geo.LineSegment.construct_from_points(a_0, a_1)) + shape_a12 = geo.Shape2D(geo.LineSegment.construct_from_points(a_1, a_2)) + profile_a = pcg.Profile([shape_a01, shape_a12]) + + b_0 = [-4, 8] + b_1 = [0, 8] + b_2 = [16, -16] + shape_b01 = geo.Shape2D(geo.LineSegment.construct_from_points(b_0, b_1)) + shape_b12 = geo.Shape2D(geo.LineSegment.construct_from_points(b_1, b_2)) + profile_b = pcg.Profile([shape_b01, shape_b12]) + + # construction with single location and interpolation + varying_profile = pcg.VaryingProfile([profile_a, profile_b], + 1, + interpol) + check_varying_profile_state(varying_profile, [0, 1]) + varying_profile = pcg.VaryingProfile([profile_a, profile_b], + [1], + [interpol]) + check_varying_profile_state(varying_profile, [0, 1]) + + # construction with location list + varying_profile = pcg.VaryingProfile([profile_a, profile_b], + [0, 1], + interpol) + check_varying_profile_state(varying_profile, [0, 1]) + + varying_profile = pcg.VaryingProfile([profile_a, profile_b, profile_a], + [1, 2], + [interpol, interpol]) + check_varying_profile_state(varying_profile, [0, 1, 2]) + + varying_profile = pcg.VaryingProfile([profile_a, profile_b, profile_a], + [0, 1, 2], + [interpol, interpol]) + check_varying_profile_state(varying_profile, [0, 1, 2]) + + # exceptions ------------------------------------------ + + # first location is not 0 + with pytest.raises(Exception): + pcg.VaryingProfile([profile_a, profile_b], [1, 2], interpol) + + # number of locations is not correct + with pytest.raises(Exception): + pcg.VaryingProfile([profile_a, profile_b, profile_a], [1], + [interpol, interpol]) + with pytest.raises(Exception): + pcg.VaryingProfile([profile_a, profile_b], [0, 1, 2], + interpol) + + # number of interpolations is not correct + with pytest.raises(Exception): + pcg.VaryingProfile([profile_a, profile_b, profile_a], [0, 1, 2], + [interpol]) + with pytest.raises(Exception): + pcg.VaryingProfile([profile_a, profile_b, profile_a], [0, 1, 2], + [interpol, interpol, interpol]) From 74678b41fc56a9673587818f2ea88aff6e17a6da Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Mon, 27 Jan 2020 16:36:03 +0100 Subject: [PATCH 100/177] Add missing testcase --- tests/test_point_cloud_generator.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/test_point_cloud_generator.py b/tests/test_point_cloud_generator.py index 587028c..fcd9558 100644 --- a/tests/test_point_cloud_generator.py +++ b/tests/test_point_cloud_generator.py @@ -499,3 +499,8 @@ def test_varying_profile_construction(): with pytest.raises(Exception): pcg.VaryingProfile([profile_a, profile_b, profile_a], [0, 1, 2], [interpol, interpol, interpol]) + + # locations not ordered + with pytest.raises(Exception): + pcg.VaryingProfile([profile_a, profile_b, profile_a], [0, 2, 1], + [interpol, interpol]) From 716b7b37c5878cde9fc73477e93219d7d2c107c5 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Tue, 28 Jan 2020 10:57:11 +0100 Subject: [PATCH 101/177] Add geometry class to replace section class --- mypackage/point_cloud_generator.py | 110 +++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) diff --git a/mypackage/point_cloud_generator.py b/mypackage/point_cloud_generator.py index d8977b3..65d4f2e 100644 --- a/mypackage/point_cloud_generator.py +++ b/mypackage/point_cloud_generator.py @@ -374,10 +374,20 @@ def __init__(self, profiles, locations, interpolation_schemes): self._locations = locations self._interpolation_schemes = interpolation_schemes + def _segment_index(self, location): + idx = 0 + while location > self._locations[idx + 1]: + idx += 1 + return idx + @property def locations(self): return self._locations + @property + def max_location(self): + return self._locations[-1] + @property def num_interpolation_schemes(self): return len(self._interpolation_schemes) @@ -390,6 +400,106 @@ def num_locations(self): def num_profiles(self): return len(self._profiles) + def local_profile(self, location): + location = np.clip(location, 0, self.max_location) + + idx = self._segment_index(location) + segment_length = self._locations[idx + 1] - self._locations[idx] + weight = (location - self._locations[idx]) / segment_length + + return self._interpolation_schemes[idx].interpolate( + self._profiles[idx], self._profiles[idx + 1], weight) + + +# Geometry class ------------------------------------------------------------- + + +class Geometry: + """Define the experimental geometry""" + + def __init__(self, profile, trace): + """ + Constructor. + + :param profile: Constant or variable profile. + :param trace: Trace + """ + self._check_inputs(profile, trace) + self._profile = profile + self._trace = trace + + def _check_inputs(self, profile, trace): + """ + Check the inputs to the constructor. + + :param profile: Constant or variable profile. + :param trace: Trace + :return: --- + """ + if not (isinstance(profile, Profile) or + isinstance(profile, VaryingProfile)): + raise TypeError( + "'profile' must be a 'Profile' or 'VariableProfile' class") + + if not isinstance(trace, Trace): + raise TypeError( + "'trace' must be a 'Trace' class") + + def _get_local_profile_data(self, trace_location, raster_width): + profile_location = trace_location / self._trace.length * \ + self._profile.max_location + profile = self._profile.local_profile(profile_location) + return self._profile_data_3d(profile, raster_width) + + def _get_trace_locations(self, raster_width): + num_raster_segments = int(np.round(self._trace.length / raster_width)) + raster_width_eff = self._trace.length / num_raster_segments + locations = np.arange(0, + self._trace.length - raster_width_eff / 2, + raster_width_eff) + return np.hstack([locations, self._trace.length]) + + def _get_transformed_profile_data(self, profile_data, location): + local_cs = self._trace.local_coordinate_system(location) + local_data = np.matmul(local_cs.basis, profile_data) + return local_data + local_cs.origin[:, np.newaxis] + + def _profile_data_3d(self, profile, raster_width): + profile_data = profile.rasterize(raster_width) + return np.insert(profile_data, 1, 0, axis=0) + + def _rasterize_constant_profile(self, raster_width): + + profile_data = self._profile_data_3d(self._profile, raster_width) + + locations = self._get_trace_locations(raster_width) + raster_data = np.empty([3, 0]) + for i in range(len(locations)): + local_data = self._get_transformed_profile_data(profile_data, + locations[i]) + raster_data = np.hstack([raster_data, local_data]) + + return raster_data + + def _rasterize_variable_profile(self, raster_width): + locations = self._get_trace_locations(raster_width) + raster_data = np.empty([3, 0]) + for i in range(len(locations)): + profile_data = self._get_local_profile_data(locations[i], + raster_width) + + local_data = self._get_transformed_profile_data(profile_data, + locations[i]) + raster_data = np.hstack([raster_data, local_data]) + + return raster_data + + def rasterize(self, raster_width): + if isinstance(self._profile, Profile): + return self._rasterize_constant_profile(raster_width) + else: + return self._rasterize_variable_profile(raster_width) + # Section class --------------------------------------------------------------- From e387aadb28b190c271e982d806c85823fa71711a Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Tue, 28 Jan 2020 11:08:12 +0100 Subject: [PATCH 102/177] Remove section class --- mypackage/point_cloud_generator.py | 76 ------------------------------ 1 file changed, 76 deletions(-) diff --git a/mypackage/point_cloud_generator.py b/mypackage/point_cloud_generator.py index 65d4f2e..1a1fc40 100644 --- a/mypackage/point_cloud_generator.py +++ b/mypackage/point_cloud_generator.py @@ -499,79 +499,3 @@ def rasterize(self, raster_width): return self._rasterize_constant_profile(raster_width) else: return self._rasterize_variable_profile(raster_width) - - -# Section class --------------------------------------------------------------- - -class Section: - """Defines a section""" - - def __init__(self, profiles, trace, profile_interpolations=None, - profile_positions=None): - - profiles = to_list(profiles) - profile_positions = to_list(profile_positions) - profile_interpolations = to_list(profile_interpolations) - - if not all(isinstance(profile, Profile) for profile in profiles): - raise TypeError( - "Only instances of lists or Shape2d objects are accepted.") - - if len(profiles) > 1 and len(profile_interpolations) != len( - profiles) - 1: - raise Exception( - "Number of interpolations must be one less than number " - "of profiles") - - if len(profiles) > 2 and len(profile_positions) != len(profiles) - 2: - raise Exception( - "If more than two profiles are used, the positions of the " - "profiles between the first and last one need to be " - "specified in a list of size: num_profiles -2.") - - self._profiles = profiles - self._interpolations = profile_interpolations - self._positions = [0] + profile_positions + [trace.length] - self.trace = trace - - def _profile_segment_index(self, position): - position = np.clip(position, 0, self._positions[-1]) - idx = 0 - while position > self._positions[idx + 1]: - idx += 1 - return idx - - def _interpolated_profile(self, position): - if len(self._profiles) == 1: - return self._profiles[0] - else: - idx = self._profile_segment_index(position) - weight = (position - self._positions[idx]) / ( - self._positions[idx + 1] - self._positions[idx]) - profile = self._interpolations[idx].interpolate( - self._profiles[idx], self._profiles[idx + 1], weight) - - return profile - - def rasterize(self, raster_width): - - raster_data = np.empty([3, 0]) - trace = self.trace - global_cs = tf.CartesianCoordinateSystem3d() - for position in np.arange(0, trace.length + 0.01, 0.5): - profile = self._interpolated_profile(position) - profile_raster_data = profile.rasterize(raster_width) - profile_raster_data = np.insert(profile_raster_data, 1, 0, axis=0) - trace_cs = trace.local_coordinate_system(position) - - rotation = tf.change_of_basis_rotation(trace_cs, global_cs) - translation = tf.change_of_basis_translation(trace_cs, global_cs) - - profile_raster_data = np.matmul(rotation, - profile_raster_data) - - profile_raster_data += translation[:, np.newaxis] - - raster_data = np.hstack((raster_data, profile_raster_data)) - - return raster_data.transpose() From 9658246f75676b84e03239688cec14cf018ee795 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Tue, 28 Jan 2020 11:11:25 +0100 Subject: [PATCH 103/177] Rename VaryingProfile to VariableProfile --- mypackage/point_cloud_generator.py | 4 +-- tests/test_point_cloud_generator.py | 52 ++++++++++++++--------------- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/mypackage/point_cloud_generator.py b/mypackage/point_cloud_generator.py index 1a1fc40..af99aa7 100644 --- a/mypackage/point_cloud_generator.py +++ b/mypackage/point_cloud_generator.py @@ -348,7 +348,7 @@ def interpolate(a, b, weight): # Varying profile class ------------------------------------------------------- -class VaryingProfile: +class VariableProfile: def __init__(self, profiles, locations, interpolation_schemes): locations = to_list(locations) interpolation_schemes = to_list(interpolation_schemes) @@ -437,7 +437,7 @@ def _check_inputs(self, profile, trace): :return: --- """ if not (isinstance(profile, Profile) or - isinstance(profile, VaryingProfile)): + isinstance(profile, VariableProfile)): raise TypeError( "'profile' must be a 'Profile' or 'VariableProfile' class") diff --git a/tests/test_point_cloud_generator.py b/tests/test_point_cloud_generator.py index fcd9558..588681e 100644 --- a/tests/test_point_cloud_generator.py +++ b/tests/test_point_cloud_generator.py @@ -453,54 +453,54 @@ def test_varying_profile_construction(): profile_b = pcg.Profile([shape_b01, shape_b12]) # construction with single location and interpolation - varying_profile = pcg.VaryingProfile([profile_a, profile_b], - 1, - interpol) + varying_profile = pcg.VariableProfile([profile_a, profile_b], + 1, + interpol) check_varying_profile_state(varying_profile, [0, 1]) - varying_profile = pcg.VaryingProfile([profile_a, profile_b], - [1], - [interpol]) + varying_profile = pcg.VariableProfile([profile_a, profile_b], + [1], + [interpol]) check_varying_profile_state(varying_profile, [0, 1]) # construction with location list - varying_profile = pcg.VaryingProfile([profile_a, profile_b], - [0, 1], - interpol) + varying_profile = pcg.VariableProfile([profile_a, profile_b], + [0, 1], + interpol) check_varying_profile_state(varying_profile, [0, 1]) - varying_profile = pcg.VaryingProfile([profile_a, profile_b, profile_a], - [1, 2], - [interpol, interpol]) + varying_profile = pcg.VariableProfile([profile_a, profile_b, profile_a], + [1, 2], + [interpol, interpol]) check_varying_profile_state(varying_profile, [0, 1, 2]) - varying_profile = pcg.VaryingProfile([profile_a, profile_b, profile_a], - [0, 1, 2], - [interpol, interpol]) + varying_profile = pcg.VariableProfile([profile_a, profile_b, profile_a], + [0, 1, 2], + [interpol, interpol]) check_varying_profile_state(varying_profile, [0, 1, 2]) # exceptions ------------------------------------------ # first location is not 0 with pytest.raises(Exception): - pcg.VaryingProfile([profile_a, profile_b], [1, 2], interpol) + pcg.VariableProfile([profile_a, profile_b], [1, 2], interpol) # number of locations is not correct with pytest.raises(Exception): - pcg.VaryingProfile([profile_a, profile_b, profile_a], [1], - [interpol, interpol]) + pcg.VariableProfile([profile_a, profile_b, profile_a], [1], + [interpol, interpol]) with pytest.raises(Exception): - pcg.VaryingProfile([profile_a, profile_b], [0, 1, 2], - interpol) + pcg.VariableProfile([profile_a, profile_b], [0, 1, 2], + interpol) # number of interpolations is not correct with pytest.raises(Exception): - pcg.VaryingProfile([profile_a, profile_b, profile_a], [0, 1, 2], - [interpol]) + pcg.VariableProfile([profile_a, profile_b, profile_a], [0, 1, 2], + [interpol]) with pytest.raises(Exception): - pcg.VaryingProfile([profile_a, profile_b, profile_a], [0, 1, 2], - [interpol, interpol, interpol]) + pcg.VariableProfile([profile_a, profile_b, profile_a], [0, 1, 2], + [interpol, interpol, interpol]) # locations not ordered with pytest.raises(Exception): - pcg.VaryingProfile([profile_a, profile_b, profile_a], [0, 2, 1], - [interpol, interpol]) + pcg.VariableProfile([profile_a, profile_b, profile_a], [0, 2, 1], + [interpol, interpol]) From 921259f66458ccbf2ecc8305c8f86416b1ab86e5 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Tue, 28 Jan 2020 11:15:06 +0100 Subject: [PATCH 104/177] Rename some variables and function in tests --- tests/test_point_cloud_generator.py | 58 ++++++++++++++--------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/tests/test_point_cloud_generator.py b/tests/test_point_cloud_generator.py index 588681e..33f3e12 100644 --- a/tests/test_point_cloud_generator.py +++ b/tests/test_point_cloud_generator.py @@ -423,19 +423,19 @@ def test_linear_profile_interpolation_sbs(): 0.2) -# test varying profile -------------------------------------------------------- +# test variable profile ------------------------------------------------------- -def check_varying_profile_state(varying_profile, locations): +def check_variable_profile_state(variable_profile, locations): num_profiles = len(locations) - assert varying_profile.num_interpolation_schemes == num_profiles - 1 - assert varying_profile.num_locations == num_profiles - assert varying_profile.num_profiles == num_profiles + assert variable_profile.num_interpolation_schemes == num_profiles - 1 + assert variable_profile.num_locations == num_profiles + assert variable_profile.num_profiles == num_profiles for i in range(num_profiles): - assert math.isclose(locations[i], varying_profile.locations[i]) + assert math.isclose(locations[i], variable_profile.locations[i]) -def test_varying_profile_construction(): +def test_variable_profile_construction(): interpol = pcg.LinearProfileInterpolationSBS a_0 = [0, 0] @@ -453,30 +453,30 @@ def test_varying_profile_construction(): profile_b = pcg.Profile([shape_b01, shape_b12]) # construction with single location and interpolation - varying_profile = pcg.VariableProfile([profile_a, profile_b], - 1, - interpol) - check_varying_profile_state(varying_profile, [0, 1]) - varying_profile = pcg.VariableProfile([profile_a, profile_b], - [1], - [interpol]) - check_varying_profile_state(varying_profile, [0, 1]) + variable_profile = pcg.VariableProfile([profile_a, profile_b], + 1, + interpol) + check_variable_profile_state(variable_profile, [0, 1]) + variable_profile = pcg.VariableProfile([profile_a, profile_b], + [1], + [interpol]) + check_variable_profile_state(variable_profile, [0, 1]) # construction with location list - varying_profile = pcg.VariableProfile([profile_a, profile_b], - [0, 1], - interpol) - check_varying_profile_state(varying_profile, [0, 1]) - - varying_profile = pcg.VariableProfile([profile_a, profile_b, profile_a], - [1, 2], - [interpol, interpol]) - check_varying_profile_state(varying_profile, [0, 1, 2]) - - varying_profile = pcg.VariableProfile([profile_a, profile_b, profile_a], - [0, 1, 2], - [interpol, interpol]) - check_varying_profile_state(varying_profile, [0, 1, 2]) + variable_profile = pcg.VariableProfile([profile_a, profile_b], + [0, 1], + interpol) + check_variable_profile_state(variable_profile, [0, 1]) + + variable_profile = pcg.VariableProfile([profile_a, profile_b, profile_a], + [1, 2], + [interpol, interpol]) + check_variable_profile_state(variable_profile, [0, 1, 2]) + + variable_profile = pcg.VariableProfile([profile_a, profile_b, profile_a], + [0, 1, 2], + [interpol, interpol]) + check_variable_profile_state(variable_profile, [0, 1, 2]) # exceptions ------------------------------------------ From 4ed32fc6e167abc2fe0c7108b248a21eb61bd21f Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Tue, 28 Jan 2020 13:25:50 +0100 Subject: [PATCH 105/177] Add test for variable profile --- tests/test_point_cloud_generator.py | 69 +++++++++++++++++++++++------ 1 file changed, 56 insertions(+), 13 deletions(-) diff --git a/tests/test_point_cloud_generator.py b/tests/test_point_cloud_generator.py index 33f3e12..cf8ae09 100644 --- a/tests/test_point_cloud_generator.py +++ b/tests/test_point_cloud_generator.py @@ -8,6 +8,25 @@ import tests.helpers as helpers +# helpers --------------------------------------------------------------------- + +def get_default_profiles(): + a_0 = [0, 0] + a_1 = [8, 16] + a_2 = [16, 0] + shape_a01 = geo.Shape2D(geo.LineSegment.construct_from_points(a_0, a_1)) + shape_a12 = geo.Shape2D(geo.LineSegment.construct_from_points(a_1, a_2)) + profile_a = pcg.Profile([shape_a01, shape_a12]) + + b_0 = [-4, 8] + b_1 = [0, 8] + b_2 = [16, -16] + shape_b01 = geo.Shape2D(geo.LineSegment.construct_from_points(b_0, b_1)) + shape_b12 = geo.Shape2D(geo.LineSegment.construct_from_points(b_1, b_2)) + profile_b = pcg.Profile([shape_b01, shape_b12]) + return [profile_a, profile_b] + + # Test profile class ---------------------------------------------------------- def test_profile_construction_and_shape_addition(): @@ -354,6 +373,7 @@ def test_trace_local_coordinate_system(): # Profile interpolation classes ----------------------------------------------- + def check_interpolated_profile_points(profile, c_0, c_1, c_2): helpers.check_vectors_identical(profile.shapes[0].segments[0].point_start, c_0) @@ -380,6 +400,8 @@ def test_linear_profile_interpolation_sbs(): shape_b12 = geo.Shape2D(geo.LineSegment.construct_from_points(b_1, b_2)) profile_b = pcg.Profile([shape_b01, shape_b12]) + [profile_a, profile_b] = get_default_profiles() + for i in range(5): weight = i / 4. profile_c = pcg.LinearProfileInterpolationSBS.interpolate(profile_a, @@ -438,19 +460,7 @@ def check_variable_profile_state(variable_profile, locations): def test_variable_profile_construction(): interpol = pcg.LinearProfileInterpolationSBS - a_0 = [0, 0] - a_1 = [8, 16] - a_2 = [16, 0] - shape_a01 = geo.Shape2D(geo.LineSegment.construct_from_points(a_0, a_1)) - shape_a12 = geo.Shape2D(geo.LineSegment.construct_from_points(a_1, a_2)) - profile_a = pcg.Profile([shape_a01, shape_a12]) - - b_0 = [-4, 8] - b_1 = [0, 8] - b_2 = [16, -16] - shape_b01 = geo.Shape2D(geo.LineSegment.construct_from_points(b_0, b_1)) - shape_b12 = geo.Shape2D(geo.LineSegment.construct_from_points(b_1, b_2)) - profile_b = pcg.Profile([shape_b01, shape_b12]) + profile_a, profile_b = get_default_profiles() # construction with single location and interpolation variable_profile = pcg.VariableProfile([profile_a, profile_b], @@ -504,3 +514,36 @@ def test_variable_profile_construction(): with pytest.raises(Exception): pcg.VariableProfile([profile_a, profile_b, profile_a], [0, 2, 1], [interpol, interpol]) + + +def test_variable_profile_local_profile(): + interpol = pcg.LinearProfileInterpolationSBS + + profile_a, profile_b = get_default_profiles() + variable_profile = pcg.VariableProfile([profile_a, profile_b, profile_a], + [0, 1, 2], + [interpol, interpol]) + + for i in range(5): + # first segment + location = i / 4. + profile = variable_profile.local_profile(location) + check_interpolated_profile_points(profile, + [-i, 2 * i], + [8 - 2 * i, 16 - 2 * i], + [16, -4 * i]) + # second segment + location += 1 + profile = variable_profile.local_profile(location) + check_interpolated_profile_points(profile, + [-4 + i, 8 - 2 * i], + [2 * i, 8 + 2 * i], + [16, -16 + 4 * i]) + + # check if values are clipped to valid range + + profile = variable_profile.local_profile(177) + check_interpolated_profile_points(profile, [0, 0], [8, 16], [16, 0]) + + profile = variable_profile.local_profile(-2) + check_interpolated_profile_points(profile, [0, 0], [8, 16], [16, 0]) From cd901636ead67c9273a302738228cb47995aa0de Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Tue, 28 Jan 2020 14:22:48 +0100 Subject: [PATCH 106/177] Test geometry construction --- mypackage/point_cloud_generator.py | 19 +++++- tests/test_point_cloud_generator.py | 97 +++++++++++++++++++++++++++-- 2 files changed, 109 insertions(+), 7 deletions(-) diff --git a/mypackage/point_cloud_generator.py b/mypackage/point_cloud_generator.py index af99aa7..8e6e08a 100644 --- a/mypackage/point_cloud_generator.py +++ b/mypackage/point_cloud_generator.py @@ -179,6 +179,7 @@ def radius(self): """ return self._radius + @property def is_clockwise(self): return self._sign_winding < 0 @@ -290,6 +291,7 @@ def segments(self): """ return self._segments + @property def num_segments(self): """ Get the number of segments. @@ -380,6 +382,10 @@ def _segment_index(self, location): idx += 1 return idx + @property + def interpolation_schemes(self): + return self._interpolation_schemes + @property def locations(self): return self._locations @@ -400,6 +406,10 @@ def num_locations(self): def num_profiles(self): return len(self._profiles) + @property + def profiles(self): + return self._profiles + def local_profile(self, location): location = np.clip(location, 0, self.max_location) @@ -413,7 +423,6 @@ def local_profile(self, location): # Geometry class ------------------------------------------------------------- - class Geometry: """Define the experimental geometry""" @@ -494,6 +503,14 @@ def _rasterize_variable_profile(self, raster_width): return raster_data + @property + def profile(self): + return self._profile + + @property + def trace(self): + return self._trace + def rasterize(self, raster_width): if isinstance(self._profile, Profile): return self._rasterize_constant_profile(raster_width) diff --git a/tests/test_point_cloud_generator.py b/tests/test_point_cloud_generator.py index cf8ae09..5623d74 100644 --- a/tests/test_point_cloud_generator.py +++ b/tests/test_point_cloud_generator.py @@ -10,6 +10,42 @@ # helpers --------------------------------------------------------------------- +def check_profiles_identical(a, b): + assert a.num_shapes == b.num_shapes + for i in range(a.num_shapes): + check_shapes_identical(a.shapes[i], b.shapes[i]) + + +def check_shapes_identical(a, b): + assert a.num_segments == b.num_segments + for i in range(a.num_segments): + assert isinstance(a.segments[i], type(b.segments[i])) + helpers.check_vectors_identical(a.segments[i].point_start, + b.segments[i].point_start) + helpers.check_vectors_identical(a.segments[i].point_end, + b.segments[i].point_end) + if isinstance(a.segments[i], geo.ArcSegment): + helpers.check_vectors_identical(a.segments[i].point_center, + b.segments[i].point_center) + + +def check_trace_segments_identical(a, b): + assert isinstance(a, type(b)) + if isinstance(a, pcg.LinearHorizontalTraceSegment): + assert a.length == b.length + else: + assert a.is_clockwise == b.is_clockwise + assert math.isclose(a.angle, b.angle) + assert math.isclose(a.length, b.length) + assert math.isclose(a.radius, b.radius) + + +def check_traces_identical(a, b): + assert a.num_segments == b.num_segments + for i in range(a.num_segments): + check_trace_segments_identical(a.segments[i], b.segments[i]) + + def get_default_profiles(): a_0 = [0, 0] a_1 = [8, 16] @@ -210,8 +246,8 @@ def test_radial_horizontal_trace_segment(): assert math.isclose(segment_ccw.angle, angle) assert math.isclose(segment_cw.radius, radius) assert math.isclose(segment_ccw.radius, radius) - assert segment_cw.is_clockwise() is True - assert segment_ccw.is_clockwise() is False + assert segment_cw.is_clockwise + assert not segment_ccw.is_clockwise # check positions for weight in np.arange(0.1, 1, 0.1): @@ -249,7 +285,7 @@ def test_trace_construction(): # test single segment construction -------------------- trace = pcg.Trace(linear_segment, ccs) assert math.isclose(trace.length, linear_segment.length) - assert trace.num_segments() == 1 + assert trace.num_segments == 1 segments = trace.segments assert len(segments) == 1 @@ -263,7 +299,7 @@ def test_trace_construction(): trace = pcg.Trace([radial_segment, linear_segment]) assert math.isclose(trace.length, linear_segment.length + radial_segment.length) - assert trace.num_segments() == 2 + assert trace.num_segments == 2 segments = trace.segments assert len(segments) == 2 @@ -272,8 +308,7 @@ def test_trace_construction(): assert math.isclose(radial_segment.radius, segments[0].radius) assert math.isclose(radial_segment.angle, segments[0].angle) - assert math.isclose(radial_segment.is_clockwise(), - segments[0].is_clockwise()) + assert math.isclose(radial_segment.is_clockwise, segments[0].is_clockwise) assert math.isclose(linear_segment.length, segments[1].length) helpers.check_matrices_identical(np.identity(3), @@ -547,3 +582,53 @@ def test_variable_profile_local_profile(): profile = variable_profile.local_profile(-2) check_interpolated_profile_points(profile, [0, 0], [8, 16], [16, 0]) + + +# test geometry class --------------------------------------------------------- + +def check_variable_profiles_identical(a, b): + assert a.num_profiles == b.num_profiles + assert a.num_locations == b.num_locations + assert a.num_interpolation_schemes == b.num_interpolation_schemes + + for i in range(a.num_profiles): + check_profiles_identical(a.profiles[i], b.profiles[i]) + for i in range(a.num_locations): + assert math.isclose(a.locations[i], b.locations[i]) + for i in range(a.num_interpolation_schemes): + assert isinstance(a.interpolation_schemes[i], + type(b.interpolation_schemes[i])) + + +def test_geometry_construction(): + profile_a, profile_b = get_default_profiles() + variable_profile = pcg.VariableProfile([profile_a, profile_b], [0, 1], + pcg.LinearProfileInterpolationSBS) + + radial_segment = pcg.RadialHorizontalTraceSegment(1, np.pi) + linear_segment = pcg.LinearHorizontalTraceSegment(1) + trace = pcg.Trace([radial_segment, linear_segment]) + + # single profile construction + geometry = pcg.Geometry(profile_a, trace) + check_profiles_identical(geometry.profile, profile_a) + check_traces_identical(geometry.trace, trace) + + # variable profile construction + geometry = pcg.Geometry(variable_profile, trace) + check_variable_profiles_identical(geometry.profile, variable_profile) + check_traces_identical(geometry.trace, trace) + + # exceptions ------------------------------------------ + + # wrong types + with pytest.raises(TypeError): + pcg.Geometry(variable_profile, profile_b) + with pytest.raises(TypeError): + pcg.Geometry(trace, trace) + with pytest.raises(TypeError): + pcg.Geometry(trace, profile_b) + with pytest.raises(TypeError): + pcg.Geometry(variable_profile, "a") + with pytest.raises(TypeError): + pcg.Geometry("42", trace) From a0aa368aaa463cb01f03aadcd127e0bacd2b1748 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Tue, 28 Jan 2020 16:33:59 +0100 Subject: [PATCH 107/177] Start testing geometry rasterization --- mypackage/point_cloud_generator.py | 24 ++++++++----- tests/test_point_cloud_generator.py | 55 +++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 9 deletions(-) diff --git a/mypackage/point_cloud_generator.py b/mypackage/point_cloud_generator.py index 8e6e08a..b04d904 100644 --- a/mypackage/point_cloud_generator.py +++ b/mypackage/point_cloud_generator.py @@ -461,6 +461,7 @@ def _get_local_profile_data(self, trace_location, raster_width): return self._profile_data_3d(profile, raster_width) def _get_trace_locations(self, raster_width): + raster_width = np.clip(raster_width, 0, self._trace.length) num_raster_segments = int(np.round(self._trace.length / raster_width)) raster_width_eff = self._trace.length / num_raster_segments locations = np.arange(0, @@ -477,11 +478,13 @@ def _profile_data_3d(self, profile, raster_width): profile_data = profile.rasterize(raster_width) return np.insert(profile_data, 1, 0, axis=0) - def _rasterize_constant_profile(self, raster_width): + def _rasterize_constant_profile(self, profile_raster_width, + trace_raster_width): - profile_data = self._profile_data_3d(self._profile, raster_width) + profile_data = self._profile_data_3d(self._profile, + profile_raster_width) - locations = self._get_trace_locations(raster_width) + locations = self._get_trace_locations(trace_raster_width) raster_data = np.empty([3, 0]) for i in range(len(locations)): local_data = self._get_transformed_profile_data(profile_data, @@ -490,12 +493,13 @@ def _rasterize_constant_profile(self, raster_width): return raster_data - def _rasterize_variable_profile(self, raster_width): - locations = self._get_trace_locations(raster_width) + def _rasterize_variable_profile(self, profile_raster_width, + trace_raster_width): + locations = self._get_trace_locations(trace_raster_width) raster_data = np.empty([3, 0]) for i in range(len(locations)): profile_data = self._get_local_profile_data(locations[i], - raster_width) + profile_raster_width) local_data = self._get_transformed_profile_data(profile_data, locations[i]) @@ -511,8 +515,10 @@ def profile(self): def trace(self): return self._trace - def rasterize(self, raster_width): + def rasterize(self, profile_raster_width, trace_raster_width): if isinstance(self._profile, Profile): - return self._rasterize_constant_profile(raster_width) + return self._rasterize_constant_profile(profile_raster_width, + trace_raster_width) else: - return self._rasterize_variable_profile(raster_width) + return self._rasterize_variable_profile(profile_raster_width, + trace_raster_width) diff --git a/tests/test_point_cloud_generator.py b/tests/test_point_cloud_generator.py index 5623d74..c980548 100644 --- a/tests/test_point_cloud_generator.py +++ b/tests/test_point_cloud_generator.py @@ -632,3 +632,58 @@ def test_geometry_construction(): pcg.Geometry(variable_profile, "a") with pytest.raises(TypeError): pcg.Geometry("42", trace) + + +def test_geometry_rasterization(): + a0 = [-1, 0] + a1 = [-1, 1] + a2 = [0, 1] + a3 = [1, 1] + a4 = [1, 0] + + shape_a012 = geo.Shape2D([geo.LineSegment.construct_from_points(a0, a1), + geo.LineSegment.construct_from_points(a1, a2)]) + shape_a234 = geo.Shape2D([geo.LineSegment.construct_from_points(a2, a3), + geo.LineSegment.construct_from_points(a3, a4)]) + + shape_b012 = copy.deepcopy(shape_a012) + shape_b234 = copy.deepcopy(shape_a234) + shape_b012.apply_transformation([[2, 0], [0, 2]]) + shape_b234.apply_transformation([[2, 0], [0, 2]]) + + profile_a = pcg.Profile([shape_a012, shape_a234]) + profile_b = pcg.Profile([shape_b012, shape_b234]) + + radial_segment = pcg.RadialHorizontalTraceSegment(1, np.pi / 2) + linear_segment = pcg.LinearHorizontalTraceSegment(1) + trace = pcg.Trace([linear_segment, radial_segment]) + + geometry = pcg.Geometry(profile_a, trace) + + data = geometry.rasterize(7, 0.1) + # Note, if raster width is larger than the segment, it is automatically + # adjusted to the segment widht. Hence each rasterized profile has 6 + # points, which were defined at the beginning of the test (a2 is + # included twice) + num_raster_profiles = int(np.round(data.shape[1] / 6)) + profile_points = np.array([a0, a1, a2, a2, a3, a4]).transpose() + + for i in range(num_raster_profiles): + idx_0 = i * 6 + if data[1, idx_0 + 2] <= 1: + y = data[1, idx_0] + if y > 0: + eff_raster_width = y / i + assert math.isclose(eff_raster_width, 0.1, abs_tol=0.01) + else: + assert math.isclose(y, 0, abs_tol=1E-9) + + for j in range(6): + assert math.isclose(data[0, idx_0 + j], profile_points[0, j]) + assert math.isclose(data[2, idx_0 + j], profile_points[1, j]) + assert math.isclose(data[1, idx_0 + j], data[1, idx_0]) + else: + continue_here = True + + +test_geometry_rasterization() From 81f697b21936b6d301f056620ce9baccc33cb43d Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Wed, 29 Jan 2020 13:58:01 +0100 Subject: [PATCH 108/177] Finish geometry rasterization test with constant profile --- tests/test_point_cloud_generator.py | 42 ++++++++++++++++++++++------- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/tests/test_point_cloud_generator.py b/tests/test_point_cloud_generator.py index c980548..fa45ad1 100644 --- a/tests/test_point_cloud_generator.py +++ b/tests/test_point_cloud_generator.py @@ -634,7 +634,7 @@ def test_geometry_construction(): pcg.Geometry("42", trace) -def test_geometry_rasterization(): +def test_geometry_rasterization_trace(): a0 = [-1, 0] a1 = [-1, 1] a2 = [0, 1] @@ -668,22 +668,46 @@ def test_geometry_rasterization(): num_raster_profiles = int(np.round(data.shape[1] / 6)) profile_points = np.array([a0, a1, a2, a2, a3, a4]).transpose() + eff_raster_width = trace.length / (data.shape[1] / 6 - 1) + arc_point_distance_on_trace = 2 * np.sin(eff_raster_width / 2) + for i in range(num_raster_profiles): idx_0 = i * 6 if data[1, idx_0 + 2] <= 1: y = data[1, idx_0] - if y > 0: - eff_raster_width = y / i - assert math.isclose(eff_raster_width, 0.1, abs_tol=0.01) - else: - assert math.isclose(y, 0, abs_tol=1E-9) + assert math.isclose(y, eff_raster_width * i, abs_tol=1E-6) for j in range(6): assert math.isclose(data[0, idx_0 + j], profile_points[0, j]) assert math.isclose(data[2, idx_0 + j], profile_points[1, j]) assert math.isclose(data[1, idx_0 + j], data[1, idx_0]) else: - continue_here = True - + assert math.isclose(data[0, idx_0], a0[0]) + assert math.isclose(data[1, idx_0], 1) + assert math.isclose(data[2, idx_0], a0[1]) + assert math.isclose(data[0, idx_0 + 1], a1[0]) + assert math.isclose(data[1, idx_0 + 1], 1) + assert math.isclose(data[2, idx_0 + 1], a1[1]) + + # z-values are constant + for j in np.arange(2, 6, 1): + assert math.isclose(data[2, idx_0 + j], profile_points[1, j]) -test_geometry_rasterization() + # all profile points in a common x-y plane + exp_radius = np.array([1, 1, 2, 2]) + vec_02 = data[0:2, idx_0 + 2] - data[0:2, idx_0] + assert math.isclose(np.linalg.norm(vec_02), exp_radius[0]) + for j in np.arange(3, 6, 1): + vec_0j = data[0:2, idx_0 + j] - data[0:2, idx_0] + assert math.isclose(np.linalg.norm(vec_0j), exp_radius[j - 2]) + unit_vec_0j = tf.normalize(vec_0j) + assert math.isclose(np.dot(unit_vec_0j, vec_02), 1) + + # check point distance between profiles + if data[1, idx_0 - 4] > 1: + exp_point_distance = arc_point_distance_on_trace * exp_radius + for j in np.arange(2, 6, 1): + point_distance = np.linalg.norm( + data[:, idx_0 + j] - data[:, idx_0 + j - 6]) + assert math.isclose(exp_point_distance[j - 2], + point_distance) From 495601e97aee8f1338ac64b8594ae597f0a8b047 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Wed, 29 Jan 2020 14:30:48 +0100 Subject: [PATCH 109/177] Finish second geometry rasterization test --- tests/test_point_cloud_generator.py | 74 +++++++++++++++++++++++++---- 1 file changed, 65 insertions(+), 9 deletions(-) diff --git a/tests/test_point_cloud_generator.py b/tests/test_point_cloud_generator.py index fa45ad1..dd190df 100644 --- a/tests/test_point_cloud_generator.py +++ b/tests/test_point_cloud_generator.py @@ -646,13 +646,7 @@ def test_geometry_rasterization_trace(): shape_a234 = geo.Shape2D([geo.LineSegment.construct_from_points(a2, a3), geo.LineSegment.construct_from_points(a3, a4)]) - shape_b012 = copy.deepcopy(shape_a012) - shape_b234 = copy.deepcopy(shape_a234) - shape_b012.apply_transformation([[2, 0], [0, 2]]) - shape_b234.apply_transformation([[2, 0], [0, 2]]) - profile_a = pcg.Profile([shape_a012, shape_a234]) - profile_b = pcg.Profile([shape_b012, shape_b234]) radial_segment = pcg.RadialHorizontalTraceSegment(1, np.pi / 2) linear_segment = pcg.LinearHorizontalTraceSegment(1) @@ -660,11 +654,12 @@ def test_geometry_rasterization_trace(): geometry = pcg.Geometry(profile_a, trace) - data = geometry.rasterize(7, 0.1) - # Note, if raster width is larger than the segment, it is automatically - # adjusted to the segment widht. Hence each rasterized profile has 6 + # Note, if the raster width is larger than the segment, it is automatically + # adjusted to the segment width. Hence each rasterized profile has 6 # points, which were defined at the beginning of the test (a2 is # included twice) + data = geometry.rasterize(7, 0.1) + num_raster_profiles = int(np.round(data.shape[1] / 6)) profile_points = np.array([a0, a1, a2, a2, a3, a4]).transpose() @@ -711,3 +706,64 @@ def test_geometry_rasterization_trace(): data[:, idx_0 + j] - data[:, idx_0 + j - 6]) assert math.isclose(exp_point_distance[j - 2], point_distance) + + +def test_geometry_rasterization_profile_interpolation(): + interpol = pcg.LinearProfileInterpolationSBS + + a0 = [-1, 0] + a1 = [-1, 1] + a2 = [0, 1] + a3 = [1, 1] + a4 = [1, 0] + + shape_a012 = geo.Shape2D([geo.LineSegment.construct_from_points(a0, a1), + geo.LineSegment.construct_from_points(a1, a2)]) + shape_a234 = geo.Shape2D([geo.LineSegment.construct_from_points(a2, a3), + geo.LineSegment.construct_from_points(a3, a4)]) + + shape_b012 = copy.deepcopy(shape_a012) + shape_b234 = copy.deepcopy(shape_a234) + shape_b012.apply_transformation([[2, 0], [0, 2]]) + shape_b234.apply_transformation([[2, 0], [0, 2]]) + + profile_a = pcg.Profile([shape_a012, shape_a234]) + profile_b = pcg.Profile([shape_b012, shape_b234]) + + variable_profile = pcg.VariableProfile([profile_a, profile_b, profile_a], + [0, 2, 6], [interpol, interpol]) + + linear_segment_l1 = pcg.LinearHorizontalTraceSegment(1) + linear_segment_l2 = pcg.LinearHorizontalTraceSegment(2) + # Note: The profile in the middle is not located at the start of the + # second segment + trace = pcg.Trace([linear_segment_l2, linear_segment_l1]) + + geometry = pcg.Geometry(variable_profile, trace) + + # Note: If the raster width is larger than the segment, it is automatically + # adjusted to the segment width. Hence each rasterized profile has 6 + # points, which were defined at the beginning of the test (a2 is + # included twice) + data = geometry.rasterize(7, 0.1) + assert data.shape[1] == 186 + + profile_points = np.array([a0, a1, a2, a2, a3, a4]).transpose() + + # check first segment + for i in range(11): + idx_0 = i * 6 + for j in range(6): + exp_point = np.array([profile_points[0, j] * (1 + i * 0.1), + i * 0.1, + profile_points[1, j] * (1 + i * 0.1)]) + helpers.check_vectors_identical(data[:, idx_0 + j], exp_point) + + # check second segment + for i in range(20): + idx_0 = (30 - i) * 6 + for j in range(6): + exp_point = np.array([profile_points[0, j] * (1 + i * 0.05), + 3 - i * 0.1, + profile_points[1, j] * (1 + i * 0.05)]) + helpers.check_vectors_identical(data[:, idx_0 + j], exp_point) From ba979361ab4cdc322f1785e16f507d070f15e434 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Wed, 29 Jan 2020 14:43:26 +0100 Subject: [PATCH 110/177] Fix flake8 issues --- mypackage/point_cloud_generator.py | 7 ++----- tests/test_geometry.py | 12 ++++++------ 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/mypackage/point_cloud_generator.py b/mypackage/point_cloud_generator.py index b04d904..cbfd9cf 100644 --- a/mypackage/point_cloud_generator.py +++ b/mypackage/point_cloud_generator.py @@ -2,7 +2,6 @@ import mypackage.geometry as geo import numpy as np -import copy import mypackage.transformations as tf @@ -64,8 +63,6 @@ def rasterize(self, raster_width): """ raster_data = np.empty([2, 0]) for shape in self._shapes: - shape_data = shape.rasterize(raster_width) - raster_data.shape raster_data = np.hstack( (raster_data, shape.rasterize(raster_width))) @@ -455,8 +452,8 @@ def _check_inputs(self, profile, trace): "'trace' must be a 'Trace' class") def _get_local_profile_data(self, trace_location, raster_width): - profile_location = trace_location / self._trace.length * \ - self._profile.max_location + relative_location = trace_location / self._trace.length + profile_location = relative_location * self._profile.max_location profile = self._profile.local_profile(profile_location) return self._profile_data_3d(profile, raster_width) diff --git a/tests/test_geometry.py b/tests/test_geometry.py index 6666576..d46444a 100644 --- a/tests/test_geometry.py +++ b/tests/test_geometry.py @@ -653,8 +653,8 @@ def check_point(point, point_ref, translation): arc_segment = shape._segments[0] arc_segment_ref = shape_ref._segments[0] - assert arc_segment.is_arc_winding_ccw() == \ - arc_segment_ref.is_arc_winding_ccw() + assert (arc_segment.is_arc_winding_ccw() == + arc_segment_ref.is_arc_winding_ccw()) check_point(arc_segment.point_start, arc_segment_ref.point_start, translation) @@ -689,8 +689,8 @@ def check_point_rotation(point, point_ref): arc_segment = shape.segments[0] arc_segment_ref = shape_ref.segments[0] - assert arc_segment.is_arc_winding_ccw() == \ - arc_segment_ref.is_arc_winding_ccw() + assert (arc_segment.is_arc_winding_ccw() == + arc_segment_ref.is_arc_winding_ccw()) check_point_rotation(arc_segment.point_start, arc_segment_ref.point_start) check_point_rotation(arc_segment.point_end, arc_segment_ref.point_end) @@ -719,8 +719,8 @@ def check_point_reflection(point, point_ref): arc_segment = shape.segments[0] arc_segment_ref = shape_ref.segments[0] - assert arc_segment.is_arc_winding_ccw() != \ - arc_segment_ref.is_arc_winding_ccw() + assert (arc_segment.is_arc_winding_ccw() != + arc_segment_ref.is_arc_winding_ccw()) check_point_reflection(arc_segment.point_start, arc_segment_ref.point_start) From 9edccbee7ede4facbf3d0bb6293173a885ce8f3c Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Wed, 29 Jan 2020 15:37:10 +0100 Subject: [PATCH 111/177] Fix pydocstyle issues --- mypackage/geometry.py | 31 +++++- mypackage/point_cloud_generator.py | 154 ++++++++++++++++++++++++++-- mypackage/transformations.py | 9 ++ setup.cfg | 2 +- tests/{helpers.py => _helpers.py} | 0 tests/test_geometry.py | 2 +- tests/test_point_cloud_generator.py | 2 +- tests/test_trasformations.py | 2 +- 8 files changed, 183 insertions(+), 19 deletions(-) rename tests/{helpers.py => _helpers.py} (100%) diff --git a/mypackage/geometry.py b/mypackage/geometry.py index 9d813fc..184c00e 100644 --- a/mypackage/geometry.py +++ b/mypackage/geometry.py @@ -9,6 +9,15 @@ # Helper functions ------------------------------------------------------------ def to_list(var): + """ + Store the passed variable into a list and return it. + + If the variable is already a list, it is returned without modification. + If 'None' is passed, the function returns an empty list. + + :param var: Arbitrary variable + :return: List + """ if isinstance(var, list): return var if var is None: @@ -129,7 +138,6 @@ def construct_from_points(cls, point_start, point_end): :param point_end: End point of the segment :return: Line segment """ - points = np.transpose(np.array([point_start, point_end], dtype=float)) return cls(points) @@ -310,6 +318,7 @@ def _calculate_arc_parameters(self): def _check_valid(self): """ Check if the segments data is valid. + :return: --- """ point_start = self.point_start @@ -492,11 +501,21 @@ def __init__(self, segments=None): :param segments: Single segment or list of segments """ - self._segments = to_list(segments) @classmethod def interpolate(cls, a, b, weight, interpolation_schemes): + """ + Interpolate 2 shapes. + + :param a: First shape + :param b: Second shape + :param weight: Weighting factor in the range [0 .. 1] where 0 is + shape a and 1 is shape b + :param interpolation_schemes: List of interpolation schemes for each + segment of the shape. + :return: Interpolated shape + """ if not a.num_segments == b.num_segments: raise Exception("Number of segments differ.") @@ -540,11 +559,16 @@ def num_segments(self): @property def segments(self): + """ + Get the shape's segments. + + :return: List of segments + """ return self._segments def add_segments(self, segments): """ - Add segments to the shape + Add segments to the shape. :param segments: Single segment or list of segments :return: --- @@ -601,7 +625,6 @@ def rasterize(self, raster_width): :param raster_width: The desired distance between two raster points :return: Array of contour points (3d) """ - segments = self._segments raster_data = np.empty([2, 0]) diff --git a/mypackage/point_cloud_generator.py b/mypackage/point_cloud_generator.py index cbfd9cf..7062eb4 100644 --- a/mypackage/point_cloud_generator.py +++ b/mypackage/point_cloud_generator.py @@ -8,6 +8,15 @@ # Helper functions ------------------------------------------------------------ def to_list(var): + """ + Store the passed variable into a list and return it. + + If the variable is already a list, it is returned without modification. + If 'None' is passed, the function returns an empty list. + + :param var: Arbitrary variable + :return: List + """ if isinstance(var, list): return var if var is None: @@ -142,7 +151,8 @@ def __init__(self, radius, angle, clockwise=False): @staticmethod def _arc_length(radius, angle): """ - Calculate the arc length + Calculate the arc length. + :param radius: Radius :param angle: Angle (rad) :return: Arc length @@ -178,6 +188,11 @@ def radius(self): @property def is_clockwise(self): + """ + Get True, if the segments winding is clockwise, False otherwise. + + :return: True or False + """ return self._sign_winding < 0 def local_coordinate_system(self, relative_position): @@ -348,7 +363,18 @@ def interpolate(a, b, weight): # Varying profile class ------------------------------------------------------- class VariableProfile: + """Class to define a profile of variable shape.""" + def __init__(self, profiles, locations, interpolation_schemes): + """ + Constructor. + + :param profiles: List of profiles. + :param locations: Ascending list of profile locations. Since the + first location needs to be 0, it can be omitted. + :param interpolation_schemes: List of interpolation schemes to + define the interpolation between two locations. + """ locations = to_list(locations) interpolation_schemes = to_list(interpolation_schemes) @@ -374,6 +400,12 @@ def __init__(self, profiles, locations, interpolation_schemes): self._interpolation_schemes = interpolation_schemes def _segment_index(self, location): + """ + Get the index of the segment at a certain location. + + :param location: Location + :return: Segment index + """ idx = 0 while location > self._locations[idx + 1]: idx += 1 @@ -381,33 +413,74 @@ def _segment_index(self, location): @property def interpolation_schemes(self): + """ + Get the interpolation schemes. + + :return: List of interpolation schemes + """ return self._interpolation_schemes @property def locations(self): + """ + Get the locations. + + :return: List of locations + """ return self._locations @property def max_location(self): + """ + Get the maximum location. + + :return: Maximum location + """ return self._locations[-1] @property def num_interpolation_schemes(self): + """ + Get the number of interpolation schemes. + + :return: Number of interpolation schemes + """ return len(self._interpolation_schemes) @property def num_locations(self): + """ + Get the number of profile locations. + + :return: Number of profile locations + """ return len(self._locations) @property def num_profiles(self): + """ + Get the number of profiles. + + :return: Number of profiles + """ return len(self._profiles) @property def profiles(self): + """ + Get the profiles. + + :return: List of profiles + """ return self._profiles def local_profile(self, location): + """ + Get the profile at the specified location. + + :param location: Location + :return: Local profile. + """ location = np.clip(location, 0, self.max_location) idx = self._segment_index(location) @@ -421,7 +494,7 @@ def local_profile(self, location): # Geometry class ------------------------------------------------------------- class Geometry: - """Define the experimental geometry""" + """Define the experimental geometry.""" def __init__(self, profile, trace): """ @@ -452,12 +525,25 @@ def _check_inputs(self, profile, trace): "'trace' must be a 'Trace' class") def _get_local_profile_data(self, trace_location, raster_width): + """ + Get a rasterized profile at a certain location on the trace. + + :param trace_location: Location on the trace + :param raster_width: Raster width + :return: + """ relative_location = trace_location / self._trace.length profile_location = relative_location * self._profile.max_location profile = self._profile.local_profile(profile_location) - return self._profile_data_3d(profile, raster_width) + return self._profile_raster_data_3d(profile, raster_width) + + def _rasterize_trace(self, raster_width): + """ + Rasterize the trace. - def _get_trace_locations(self, raster_width): + :param raster_width: Raster width + :return: Raster data + """ raster_width = np.clip(raster_width, 0, self._trace.length) num_raster_segments = int(np.round(self._trace.length / raster_width)) raster_width_eff = self._trace.length / num_raster_segments @@ -466,22 +552,44 @@ def _get_trace_locations(self, raster_width): raster_width_eff) return np.hstack([locations, self._trace.length]) - def _get_transformed_profile_data(self, profile_data, location): + def _get_transformed_profile_data(self, profile_raster_data, location): + """ + Transform a profiles data to a specified location on the trace. + + :param profile_raster_data: Rasterized profile + :param location: Location on the trace + :return: Transformed profile data + """ local_cs = self._trace.local_coordinate_system(location) - local_data = np.matmul(local_cs.basis, profile_data) + local_data = np.matmul(local_cs.basis, profile_raster_data) return local_data + local_cs.origin[:, np.newaxis] - def _profile_data_3d(self, profile, raster_width): + def _profile_raster_data_3d(self, profile, raster_width): + """ + Get the rasterized profile in 3d. + + The profile is located in the x-z-plane. + + :param profile: Profile + :param raster_width: Raster width + :return: Rasterized profile in 3d + """ profile_data = profile.rasterize(raster_width) return np.insert(profile_data, 1, 0, axis=0) def _rasterize_constant_profile(self, profile_raster_width, trace_raster_width): + """ + Rasterize the geometry with a constant profile. - profile_data = self._profile_data_3d(self._profile, - profile_raster_width) + :param profile_raster_width: Raster width of the profiles + :param trace_raster_width: Distance between two profiles + :return: Raster data + """ + profile_data = self._profile_raster_data_3d(self._profile, + profile_raster_width) - locations = self._get_trace_locations(trace_raster_width) + locations = self._rasterize_trace(trace_raster_width) raster_data = np.empty([3, 0]) for i in range(len(locations)): local_data = self._get_transformed_profile_data(profile_data, @@ -492,7 +600,14 @@ def _rasterize_constant_profile(self, profile_raster_width, def _rasterize_variable_profile(self, profile_raster_width, trace_raster_width): - locations = self._get_trace_locations(trace_raster_width) + """ + Rasterize the geometry with a variable profile. + + :param profile_raster_width: Raster width of the profiles + :param trace_raster_width: Distance between two profiles + :return: Raster data + """ + locations = self._rasterize_trace(trace_raster_width) raster_data = np.empty([3, 0]) for i in range(len(locations)): profile_data = self._get_local_profile_data(locations[i], @@ -506,13 +621,30 @@ def _rasterize_variable_profile(self, profile_raster_width, @property def profile(self): + """ + Get the geometry's profile. + + :return: Profile + """ return self._profile @property def trace(self): + """ + Get the geometry's trace. + + :return: Trace + """ return self._trace def rasterize(self, profile_raster_width, trace_raster_width): + """ + Rasterize the geometry. + + :param profile_raster_width: Raster width of the profiles + :param trace_raster_width: Distance between two profiles + :return: Raster data + """ if isinstance(self._profile, Profile): return self._rasterize_constant_profile(profile_raster_width, trace_raster_width) diff --git a/mypackage/transformations.py b/mypackage/transformations.py index 655c1f1..717a125 100644 --- a/mypackage/transformations.py +++ b/mypackage/transformations.py @@ -1,3 +1,5 @@ +"""Contains methods and classes for coordinate transformations.""" + import numpy as np import math from scipy.spatial.transform import Rotation as Rot @@ -133,6 +135,13 @@ def change_of_basis_rotation(ccs_from, ccs_to): def change_of_basis_translation(ccs_from, ccs_to): + """ + Calculate the translative transformation between 2 coordinate systems. + + :param ccs_from: Source coordinate system + :param ccs_to: Target coordinate system + :return: Translation vector + """ return ccs_from.origin - ccs_to.origin diff --git a/setup.cfg b/setup.cfg index 215e82c..ee721b1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -11,5 +11,5 @@ select = C,E,F,W,B,B950 # black formating options [pydocstyle] -match = (?!test_)(?!__).*\.py +match = (?!test_)(?!__)(?!_helpers).*\.py match_dir = [^\.][^\docs].* \ No newline at end of file diff --git a/tests/helpers.py b/tests/_helpers.py similarity index 100% rename from tests/helpers.py rename to tests/_helpers.py diff --git a/tests/test_geometry.py b/tests/test_geometry.py index d46444a..b243d30 100644 --- a/tests/test_geometry.py +++ b/tests/test_geometry.py @@ -3,7 +3,7 @@ import numpy as np import math import copy -import tests.helpers as helper +import tests._helpers as helper def test_vector_points_to_left_of_vector(): diff --git a/tests/test_point_cloud_generator.py b/tests/test_point_cloud_generator.py index dd190df..8e7557b 100644 --- a/tests/test_point_cloud_generator.py +++ b/tests/test_point_cloud_generator.py @@ -5,7 +5,7 @@ import math import numpy as np import copy -import tests.helpers as helpers +import tests._helpers as helpers # helpers --------------------------------------------------------------------- diff --git a/tests/test_trasformations.py b/tests/test_trasformations.py index 011cd49..9e8c405 100644 --- a/tests/test_trasformations.py +++ b/tests/test_trasformations.py @@ -4,7 +4,7 @@ import random import math import copy -import tests.helpers as helper +import tests._helpers as helper # helpers for tests ----------------------------------------------------------- From 9080306f2f800732405af9ce16fa2157f4d5a58c Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Wed, 29 Jan 2020 15:48:47 +0100 Subject: [PATCH 112/177] Fix a codacy issue --- tests/test_trasformations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_trasformations.py b/tests/test_trasformations.py index 9e8c405..f8e62cf 100644 --- a/tests/test_trasformations.py +++ b/tests/test_trasformations.py @@ -121,7 +121,7 @@ def test_single_axis_rotation_matrices(): def test_normalize(): - for i in range(20): + for _ in range(20): vec = random_non_unit_vector() unit = tf.normalize(vec) From 860e8268cb71d18d6d141f5c1760fd1817362fac Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Wed, 29 Jan 2020 15:50:51 +0100 Subject: [PATCH 113/177] Fix a codacy issue --- tests/test_trasformations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_trasformations.py b/tests/test_trasformations.py index f8e62cf..177e53a 100644 --- a/tests/test_trasformations.py +++ b/tests/test_trasformations.py @@ -254,7 +254,7 @@ def test_change_of_basis_rotation(): def test_change_of_basis_translation(): - for i in range(20): + for _ in range(20): origin_from = random_non_unit_vector() origin_to = random_non_unit_vector() base_from = rotated_positive_orthogonal_base(*random_non_unit_vector()) From 47ae411747f7d782f8159af804c4d4832025bd87 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Wed, 29 Jan 2020 15:55:04 +0100 Subject: [PATCH 114/177] Fix some codacy issues --- mypackage/point_cloud_generator.py | 6 ++++-- tests/test_point_cloud_generator.py | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/mypackage/point_cloud_generator.py b/mypackage/point_cloud_generator.py index 7062eb4..f1922c3 100644 --- a/mypackage/point_cloud_generator.py +++ b/mypackage/point_cloud_generator.py @@ -507,7 +507,8 @@ def __init__(self, profile, trace): self._profile = profile self._trace = trace - def _check_inputs(self, profile, trace): + @staticmethod + def _check_inputs(profile, trace): """ Check the inputs to the constructor. @@ -564,7 +565,8 @@ def _get_transformed_profile_data(self, profile_raster_data, location): local_data = np.matmul(local_cs.basis, profile_raster_data) return local_data + local_cs.origin[:, np.newaxis] - def _profile_raster_data_3d(self, profile, raster_width): + @staticmethod + def _profile_raster_data_3d(profile, raster_width): """ Get the rasterized profile in 3d. diff --git a/tests/test_point_cloud_generator.py b/tests/test_point_cloud_generator.py index 8e7557b..8499b26 100644 --- a/tests/test_point_cloud_generator.py +++ b/tests/test_point_cloud_generator.py @@ -326,7 +326,8 @@ def test_trace_construction(): # check construction with custom segment -------------- class CustomSegment(): - def local_coordinate_system(self, *args): + @staticmethod + def local_coordinate_system(*args): return tf.CartesianCoordinateSystem3d custom_segment = CustomSegment() From 219140a1b60bbf3ae914a35a05533f6f3a737377 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Wed, 29 Jan 2020 15:57:56 +0100 Subject: [PATCH 115/177] Fix a deepsource issue --- mypackage/point_cloud_generator.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mypackage/point_cloud_generator.py b/mypackage/point_cloud_generator.py index f1922c3..d1c94c7 100644 --- a/mypackage/point_cloud_generator.py +++ b/mypackage/point_cloud_generator.py @@ -516,8 +516,7 @@ def _check_inputs(profile, trace): :param trace: Trace :return: --- """ - if not (isinstance(profile, Profile) or - isinstance(profile, VariableProfile)): + if not isinstance(profile, (Profile, VariableProfile)): raise TypeError( "'profile' must be a 'Profile' or 'VariableProfile' class") From 7d4cd2672254625b93c8e0695111628cf242ad08 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Wed, 29 Jan 2020 15:59:27 +0100 Subject: [PATCH 116/177] Fix a deepsource issue --- mypackage/point_cloud_generator.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/mypackage/point_cloud_generator.py b/mypackage/point_cloud_generator.py index d1c94c7..819469f 100644 --- a/mypackage/point_cloud_generator.py +++ b/mypackage/point_cloud_generator.py @@ -649,6 +649,5 @@ def rasterize(self, profile_raster_width, trace_raster_width): if isinstance(self._profile, Profile): return self._rasterize_constant_profile(profile_raster_width, trace_raster_width) - else: - return self._rasterize_variable_profile(profile_raster_width, - trace_raster_width) + return self._rasterize_variable_profile(profile_raster_width, + trace_raster_width) From ea58fb3676ffeaf704d8efeb4bfc529b417894ca Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Wed, 29 Jan 2020 16:08:25 +0100 Subject: [PATCH 117/177] Fix some deepsource issues --- mypackage/point_cloud_generator.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/mypackage/point_cloud_generator.py b/mypackage/point_cloud_generator.py index 819469f..37d384b 100644 --- a/mypackage/point_cloud_generator.py +++ b/mypackage/point_cloud_generator.py @@ -258,8 +258,8 @@ def _create_lookups(self, coordinate_system_start): # Fill length lookups total_length = 0 - for i in range(len(segments)): - segment_length = segments[i].length + for _, segment in enumerate(segments): + segment_length = segment.length total_length += segment_length self._segment_length_lookup += [segment_length] self._total_length_lookup += [total_length] @@ -592,9 +592,9 @@ def _rasterize_constant_profile(self, profile_raster_width, locations = self._rasterize_trace(trace_raster_width) raster_data = np.empty([3, 0]) - for i in range(len(locations)): + for _, location in enumerate(locations): local_data = self._get_transformed_profile_data(profile_data, - locations[i]) + location) raster_data = np.hstack([raster_data, local_data]) return raster_data @@ -610,12 +610,12 @@ def _rasterize_variable_profile(self, profile_raster_width, """ locations = self._rasterize_trace(trace_raster_width) raster_data = np.empty([3, 0]) - for i in range(len(locations)): - profile_data = self._get_local_profile_data(locations[i], + for _, location in enumerate(locations): + profile_data = self._get_local_profile_data(location, profile_raster_width) local_data = self._get_transformed_profile_data(profile_data, - locations[i]) + location) raster_data = np.hstack([raster_data, local_data]) return raster_data From 7bbfd48909127f6126c097355155749a6d4407ad Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Wed, 29 Jan 2020 16:14:46 +0100 Subject: [PATCH 118/177] Fix some deepsource issues --- mypackage/geometry.py | 4 ++++ tests/test_point_cloud_generator.py | 3 +++ 2 files changed, 7 insertions(+) diff --git a/mypackage/geometry.py b/mypackage/geometry.py index 184c00e..9896590 100644 --- a/mypackage/geometry.py +++ b/mypackage/geometry.py @@ -276,6 +276,10 @@ def __init__(self, points, arc_winding_ccw=True): else: self._sign_arc_winding = -1 self._points = points + + self._arc_angle = None + self._arc_length = None + self._radius = None self._calculate_arc_parameters() def _calculate_arc_angle(self): diff --git a/tests/test_point_cloud_generator.py b/tests/test_point_cloud_generator.py index 8499b26..ec4f23a 100644 --- a/tests/test_point_cloud_generator.py +++ b/tests/test_point_cloud_generator.py @@ -326,6 +326,9 @@ def test_trace_construction(): # check construction with custom segment -------------- class CustomSegment(): + def __init__(self): + self.length = None + @staticmethod def local_coordinate_system(*args): return tf.CartesianCoordinateSystem3d From 67c40bd663182a59141601326b0c87165e27d7c6 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Wed, 29 Jan 2020 16:21:18 +0100 Subject: [PATCH 119/177] Fix some deepsource issues --- mypackage/point_cloud_generator.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mypackage/point_cloud_generator.py b/mypackage/point_cloud_generator.py index 37d384b..9634a36 100644 --- a/mypackage/point_cloud_generator.py +++ b/mypackage/point_cloud_generator.py @@ -1,7 +1,7 @@ """Contains methods and classes to generate 3d point clouds.""" -import mypackage.geometry as geo import numpy as np +import mypackage.geometry as geo import mypackage.transformations as tf @@ -272,9 +272,10 @@ def _get_segment_index(self, position): :return: Segment index """ position = np.clip(position, 0, self.length) - for i in range(len(self._total_length_lookup) - 1): + for i in range(len(self._total_length_lookup) - 2): if position <= self._total_length_lookup[i + 1]: return i + return self.num_segments - 1 @property def coordinate_system(self): From b8ede487852704471cf11061b5891210053dc59b Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Wed, 29 Jan 2020 16:31:19 +0100 Subject: [PATCH 120/177] Fix some deepsource issues --- mypackage/transformations.py | 97 ++++++++++++++++++++---------------- 1 file changed, 54 insertions(+), 43 deletions(-) diff --git a/mypackage/transformations.py b/mypackage/transformations.py index 717a125..358ce6e 100644 --- a/mypackage/transformations.py +++ b/mypackage/transformations.py @@ -37,20 +37,20 @@ def rotation_matrix_z(angle): return Rot.from_euler("z", angle).as_dcm() -def normalize(u): +def normalize(vec): """ Normalize a vector. - :param u: Vector + :param vec: Vector :return: Normalized vector """ - norm = np.linalg.norm(u) + norm = np.linalg.norm(vec) if norm == 0.: raise Exception("Vector length is 0.") - return u / norm + return vec / norm -def orientation_point_plane_containing_origin(point, a, b): +def orientation_point_plane_containing_origin(point, p_a, p_b): """ Determine a points orientation relative to a plane containing the origin. @@ -68,20 +68,20 @@ def orientation_point_plane_containing_origin(point, a, b): vectors spanning the plane. :param point: Point - :param a: Second point of the triangle 'origin - A - B'. - :param b: Third point of the triangle 'origin - A - B'. + :param p_a: Second point of the triangle 'origin - A - B'. + :param p_b: Third point of the triangle 'origin - A - B'. :return: 1, -1 or 0 (see description) """ - if (math.isclose(np.linalg.norm(a), 0) or - math.isclose(np.linalg.norm(b), 0) or - math.isclose(np.linalg.norm(b - a), 0)): + if (math.isclose(np.linalg.norm(p_a), 0) or + math.isclose(np.linalg.norm(p_b), 0) or + math.isclose(np.linalg.norm(p_b - p_a), 0)): raise Exception( "One or more points describing the plane are identical.") - return np.sign(np.linalg.det([a, b, point])) + return np.sign(np.linalg.det([p_a, p_b, point])) -def orientation_point_plane(point, a, b, c): +def orientation_point_plane(point, p_a, p_b, p_c): """ Determine a points orientation relative to an arbitrary plane. @@ -96,31 +96,32 @@ def orientation_point_plane(point, a, b, c): a plane since it has no tolerance to compensate for numerical errors. :param point: Point - :param a: First point of the triangle 'A - B - C'. - :param b: Second point of the triangle 'A - B - C'. - :param c: Third point of the triangle 'A - B - C'. + :param p_a: First point of the triangle 'A - B - C'. + :param p_b: Second point of the triangle 'A - B - C'. + :param p_c: Third point of the triangle 'A - B - C'. :return: 1, -1 or 0 (see description) """ - vec_a_b = b - a - vec_a_c = c - a - vec_a_point = point - a + vec_a_b = p_b - p_a + vec_a_c = p_c - p_a + vec_a_point = point - p_a return orientation_point_plane_containing_origin(vec_a_point, vec_a_b, vec_a_c) -def is_orthogonal(u, v, tolerance=1E-9): +def is_orthogonal(vec_u, vec_v, tolerance=1E-9): """ Check if vectors are orthogonal. - :param u: First vector - :param v: Second vector + :param vec_u: First vector + :param vec_v: Second vector :param tolerance: Numerical tolerance :return: True or False """ - if math.isclose(np.dot(u, u), 0) or math.isclose(np.dot(v, v), 0): + if math.isclose(np.dot(vec_u, vec_u), 0) or math.isclose( + np.dot(vec_v, vec_v), 0): raise Exception("One or both vectors have zero length.") - return math.isclose(np.dot(u, v), 0, abs_tol=tolerance) + return math.isclose(np.dot(vec_u, vec_v), 0, abs_tol=tolerance) def change_of_basis_rotation(ccs_from, ccs_to): @@ -210,71 +211,81 @@ def construct_from_basis(cls, basis, origin=np.array([0, 0, 0])): return cls(basis, origin=origin) @classmethod - def construct_from_xyz(cls, x, y, z, origin=np.array([0, 0, 0])): + def construct_from_xyz(cls, vec_x, vec_y, vec_z, + origin=np.array([0, 0, 0])): """ Construct a cartesian coordinate system. - :param x: Vector defining the x-axis - :param y: Vector defining the y-axis - :param z: Vector defining the z-axis + :param vec_x: Vector defining the x-axis + :param vec_y: Vector defining the y-axis + :param vec_z: Vector defining the z-axis :param origin: Position of the origin :return: Cartesian coordinate system """ - basis = np.transpose([x, y, z]) + basis = np.transpose([vec_x, vec_y, vec_z]) return cls(basis, origin=origin) @classmethod - def construct_from_xy_and_orientation(cls, x, y, positive_orientation=True, + def construct_from_xy_and_orientation(cls, vec_x, vec_y, + positive_orientation=True, origin=np.array([0, 0, 0])): """ Construct a cartesian coordinate system. - :param x: Vector defining the x-axis - :param y: Vector defining the y-axis + :param vec_x: Vector defining the x-axis + :param vec_y: Vector defining the y-axis :param positive_orientation: Set to True if the orientation should be positive and to False if not :param origin: Position of the origin :return: Cartesian coordinate system """ - z = cls._calcualte_orthogonal_axis(x, y) * cls._sign_orientation( + vec_z = cls._calcualte_orthogonal_axis(vec_x, + vec_y) * cls._sign_orientation( positive_orientation) - basis = np.transpose([x, y, z]) + + basis = np.transpose([vec_x, vec_y, vec_z]) return cls(basis, origin=origin) @classmethod - def construct_from_yz_and_orientation(cls, y, z, positive_orientation=True, + def construct_from_yz_and_orientation(cls, vec_y, vec_z, + positive_orientation=True, origin=np.array([0, 0, 0])): """ Construct a cartesian coordinate system. - :param y: Vector defining the y-axis - :param z: Vector defining the z-axis + :param vec_y: Vector defining the y-axis + :param vec_z: Vector defining the z-axis :param positive_orientation: Set to True if the orientation should be positive and to False if not :param origin: Position of the origin :return: Cartesian coordinate system """ - x = cls._calcualte_orthogonal_axis(y, z) * cls._sign_orientation( + vec_x = cls._calcualte_orthogonal_axis(vec_y, + vec_z) * cls._sign_orientation( positive_orientation) - basis = np.transpose(np.array([x, y, z])) + + basis = np.transpose(np.array([vec_x, vec_y, vec_z])) return cls(basis, origin=origin) @classmethod - def construct_from_xz_and_orientation(cls, x, z, positive_orientation=True, + def construct_from_xz_and_orientation(cls, vec_x, vec_z, + positive_orientation=True, origin=np.array([0, 0, 0])): """ Construct a cartesian coordinate system. - :param x: Vector defining the x-axis - :param z: Vector defining the z-axis + :param vec_x: Vector defining the x-axis + :param vec_z: Vector defining the z-axis :param positive_orientation: Set to True if the orientation should be positive and to False if not :param origin: Position of the origin :return: Cartesian coordinate system """ - y = cls._calcualte_orthogonal_axis(z, x) * cls._sign_orientation( + vec_y = cls._calcualte_orthogonal_axis(vec_z, + vec_x) * cls._sign_orientation( positive_orientation) - basis = np.transpose([x, y, z]) + + basis = np.transpose([vec_x, vec_y, vec_z]) return cls(basis, origin=origin) @staticmethod From b7a3c5c00bce6729e4288460594e0ef0da8cc645 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Wed, 29 Jan 2020 16:43:46 +0100 Subject: [PATCH 121/177] Fix last deepsource issues --- mypackage/geometry.py | 51 +++++++++++++++--------------- mypackage/point_cloud_generator.py | 18 +++++------ mypackage/transformations.py | 4 +-- 3 files changed, 37 insertions(+), 36 deletions(-) diff --git a/mypackage/geometry.py b/mypackage/geometry.py index 9896590..0081c1e 100644 --- a/mypackage/geometry.py +++ b/mypackage/geometry.py @@ -142,21 +142,21 @@ def construct_from_points(cls, point_start, point_end): return cls(points) @classmethod - def linear_interpolation(cls, a, b, weight): + def linear_interpolation(cls, segment_a, segment_b, weight): """ Interpolate two line segments linearly. - :param a: First segment - :param b: Second segment + :param segment_a: First segment + :param segment_b: Second segment :param weight: Weighting factor in the range [0 .. 1] where 0 is segment a and 1 is segment b :return: Interpolated segment """ - if not isinstance(a, cls) or not isinstance(b, cls): + if not isinstance(segment_a, cls) or not isinstance(segment_b, cls): raise TypeError("Parameters a and b must both be line segments.") weight = np.clip(weight, 0, 1) - points = (1 - weight) * a.points + weight * b.points + points = (1 - weight) * segment_a.points + weight * segment_b.points return cls(points) @property @@ -356,12 +356,12 @@ def construct_from_points(cls, point_start, point_end, point_center, return cls(points, arc_winding_ccw) @classmethod - def linear_interpolation(cls, a, b, weight): + def linear_interpolation(cls, segment_a, segment_b, weight): """ Interpolate two arc segments linearly. - :param a: First segment - :param b: Second segment + :param segment_a: First segment + :param segment_b: Second segment :param weight: Weighting factor in the range [0 .. 1] where 0 is segment a and 1 is segment b :return: Interpolated segment @@ -508,49 +508,49 @@ def __init__(self, segments=None): self._segments = to_list(segments) @classmethod - def interpolate(cls, a, b, weight, interpolation_schemes): + def interpolate(cls, shape_a, shape_b, weight, interpolation_schemes): """ Interpolate 2 shapes. - :param a: First shape - :param b: Second shape + :param shape_a: First shape + :param shape_b: Second shape :param weight: Weighting factor in the range [0 .. 1] where 0 is shape a and 1 is shape b :param interpolation_schemes: List of interpolation schemes for each segment of the shape. :return: Interpolated shape """ - if not a.num_segments == b.num_segments: + if not shape_a.num_segments == shape_b.num_segments: raise Exception("Number of segments differ.") weight = np.clip(weight, 0, 1) segments_c = [] - for i in range(a.num_segments): - segments_c += [interpolation_schemes[i](a.segments[i], - b.segments[i], + for i in range(shape_a.num_segments): + segments_c += [interpolation_schemes[i](shape_a.segments[i], + shape_b.segments[i], weight)] return cls(segments_c) @classmethod - def linear_interpolation(cls, a, b, weight): + def linear_interpolation(cls, shape_a, shape_b, weight): """ Interpolate 2 shapes linearly. Each segment is interpolated individually, using the corresponding linear segment interpolation. - :param a: First shape - :param b: Second shape + :param shape_a: First shape + :param shape_b: Second shape :param weight: Weighting factor in the range [0 .. 1] where 0 is shape a and 1 is shape b :return: Interpolated shape """ interpolation_schemes = [] - for i in range(a.num_segments): - interpolation_schemes += [a.segments[i].linear_interpolation] + for i in range(shape_a.num_segments): + interpolation_schemes += [shape_a.segments[i].linear_interpolation] - return cls.interpolate(a, b, weight, interpolation_schemes) + return cls.interpolate(shape_a, shape_b, weight, interpolation_schemes) @property def num_segments(self): @@ -598,11 +598,12 @@ def reflect(self, reflection_normal, distance_to_origin=0): origin :return: --- """ - v = np.array(reflection_normal, float) - dot_product = np.dot(v, v) - householder_matrix = np.identity(2) - 2 / dot_product * np.outer(v, v) + normal = np.array(reflection_normal, float) + dot_product = np.dot(normal, normal) + outer_product = np.outer(normal, normal) + householder_matrix = np.identity(2) - 2 / dot_product * outer_product - offset = v / np.sqrt(dot_product) * distance_to_origin + offset = normal / np.sqrt(dot_product) * distance_to_origin self.translate(-offset) self.apply_transformation(householder_matrix) diff --git a/mypackage/point_cloud_generator.py b/mypackage/point_cloud_generator.py index 9634a36..616e006 100644 --- a/mypackage/point_cloud_generator.py +++ b/mypackage/point_cloud_generator.py @@ -253,8 +253,8 @@ def _create_lookups(self, coordinate_system_start): # Fill coordinate system lookup for i in range(len(segments) - 1): lcs_segment_end = segments[i].local_coordinate_system(1) - cs = self._coordinate_system_lookup[i] + lcs_segment_end - self._coordinate_system_lookup += [cs] + csys = self._coordinate_system_lookup[i] + lcs_segment_end + self._coordinate_system_lookup += [csys] # Fill length lookups total_length = 0 @@ -338,24 +338,24 @@ class LinearProfileInterpolationSBS: """Linear segment by segment interpolation class for profiles.""" @staticmethod - def interpolate(a, b, weight): + def interpolate(profile_a, profile_b, weight): """ Interpolate 2 profiles. - :param a: First profile - :param b: Second profile + :param profile_a: First profile + :param profile_b: Second profile :param weight: Weighting factor [0 .. 1]. If 0, the profile is identical to 'a' and if 1, it is identical to b. :return: Interpolated profile """ weight = np.clip(weight, 0, 1) - if not len(a.shapes) == len(b.shapes): + if not len(profile_a.shapes) == len(profile_b.shapes): raise Exception("Number of profile shapes do not match.") shapes_c = [] - for i in range(a.num_shapes): - shapes_c += [geo.Shape2D.linear_interpolation(a.shapes[i], - b.shapes[i], + for i in range(profile_a.num_shapes): + shapes_c += [geo.Shape2D.linear_interpolation(profile_a.shapes[i], + profile_b.shapes[i], weight)] return Profile(shapes_c) diff --git a/mypackage/transformations.py b/mypackage/transformations.py index 358ce6e..70c3c19 100644 --- a/mypackage/transformations.py +++ b/mypackage/transformations.py @@ -303,7 +303,7 @@ def _sign_orientation(positive_orientation): return -1 @staticmethod - def _calcualte_orthogonal_axis(a0, a1): + def _calcualte_orthogonal_axis(a_0, a_1): """ Calculate an axis which is orthogonal to two other axes. @@ -314,7 +314,7 @@ def _calcualte_orthogonal_axis(a0, a1): :param a1: Second axis :return: Orthogonal axis """ - return np.cross(a0, a1) + return np.cross(a_0, a_1) @property def basis(self): From 857ea64f193b6e0a061b5c1152204308f1731fe3 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Thu, 30 Jan 2020 10:18:02 +0100 Subject: [PATCH 122/177] Refactor coordinate system class --- mypackage/transformations.py | 59 +++++++++++++++++++++++++++--------- tests/test_trasformations.py | 4 +-- 2 files changed, 47 insertions(+), 16 deletions(-) diff --git a/mypackage/transformations.py b/mypackage/transformations.py index 70c3c19..8194c1f 100644 --- a/mypackage/transformations.py +++ b/mypackage/transformations.py @@ -156,7 +156,11 @@ def __init__(self, basis=np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]]), """ Construct a cartesian coordinate system. - :param basis: List of basis vectors + :param basis: Matrix of 3 orthogonal column vectors which represent + the coordinate systems basis. Keep in mind, that the columns of the + corresponding orientation matrix is equal to the normalized basis + vectors. So each orthogonal transformation matrix can also be + provided as basis. :param origin: Position of the origin :return: Cartesian coordinate system """ @@ -170,9 +174,9 @@ def __init__(self, basis=np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]]), is_orthogonal(basis[:, 2], basis[:, 0])): raise Exception("Basis vectors must be orthogonal") - self._basis = basis + self._orientation = basis - self._origin = np.array(origin) + self._location = np.array(origin) def __add__(self, rhs_cs): """ @@ -200,21 +204,22 @@ def __add__(self, rhs_cs): return CartesianCoordinateSystem3d(basis, origin) @classmethod - def construct_from_basis(cls, basis, origin=np.array([0, 0, 0])): + def construct_from_orientation(cls, orientation, + origin=np.array([0, 0, 0])): """ - Construct a cartesian coordinate system. + Construct a cartesian coordinate system from orientation matrix. - :param basis: List of basis vectors + :param orientation: Orthogonal transformation matrix :param origin: Position of the origin :return: Cartesian coordinate system """ - return cls(basis, origin=origin) + return cls(orientation, origin=origin) @classmethod def construct_from_xyz(cls, vec_x, vec_y, vec_z, origin=np.array([0, 0, 0])): """ - Construct a cartesian coordinate system. + Construct a cartesian coordinate system from 3 basis vectors. :param vec_x: Vector defining the x-axis :param vec_y: Vector defining the y-axis @@ -230,7 +235,7 @@ def construct_from_xy_and_orientation(cls, vec_x, vec_y, positive_orientation=True, origin=np.array([0, 0, 0])): """ - Construct a cartesian coordinate system. + Construct a coordinate system from 2 vectors and an orientation. :param vec_x: Vector defining the x-axis :param vec_y: Vector defining the y-axis @@ -251,7 +256,7 @@ def construct_from_yz_and_orientation(cls, vec_y, vec_z, positive_orientation=True, origin=np.array([0, 0, 0])): """ - Construct a cartesian coordinate system. + Construct a coordinate system from 2 vectors and an orientation. :param vec_y: Vector defining the y-axis :param vec_z: Vector defining the z-axis @@ -272,7 +277,7 @@ def construct_from_xz_and_orientation(cls, vec_x, vec_z, positive_orientation=True, origin=np.array([0, 0, 0])): """ - Construct a cartesian coordinate system. + Construct a coordinate system from 2 vectors and an orientation. :param vec_x: Vector defining the x-axis :param vec_z: Vector defining the z-axis @@ -319,20 +324,46 @@ def _calcualte_orthogonal_axis(a_0, a_1): @property def basis(self): """ - Get the coordinate systems basis. + Get the normalizes basis as matrix of 3 column vectors. + + This function is identical to the 'orientation' function. :return: Basis of the coordinate system """ - return self._basis + return self._orientation + + @property + def orientation(self): + """ + Get the coordinate systems orientation matrix. + + This function is identical to the 'basis' function. + + :return: Orientation matrix + """ + return self._orientation @property def origin(self): """ Get the coordinate systems origin. + This function is identical to the 'location' function. + :return: Origin of the coordinate system """ - return self._origin + return self._location + + @property + def location(self): + """ + Get the coordinate systems location. + + This function is identical to the 'origin' function. + + :return: Location of the coordinate system. + """ + return self._location # def vector_to_vector_transformation(u, v): # r = np.cross(u, v) diff --git a/tests/test_trasformations.py b/tests/test_trasformations.py index 177e53a..53d4b82 100644 --- a/tests/test_trasformations.py +++ b/tests/test_trasformations.py @@ -288,8 +288,8 @@ def test_cartesian_coordinate_system_construction(): # construction with basis ----------------------------- - ccs_basis_pos = cls_ccs.construct_from_basis(basis_pos, origin) - ccs_basis_neg = cls_ccs.construct_from_basis(basis_neg, origin) + ccs_basis_pos = cls_ccs.construct_from_orientation(basis_pos, origin) + ccs_basis_neg = cls_ccs.construct_from_orientation(basis_neg, origin) check_coordinate_system(ccs_basis_pos, basis_pos, origin, True) check_coordinate_system(ccs_basis_neg, basis_neg, origin, False) From 94735c063384a5ad40cf3d0860f5265473a29f63 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Thu, 30 Jan 2020 10:53:26 +0100 Subject: [PATCH 123/177] Shorten corrdinate system class name --- mypackage/point_cloud_generator.py | 11 +++++------ mypackage/transformations.py | 6 +++--- tests/_helpers.py | 2 +- tests/test_point_cloud_generator.py | 6 +++--- tests/test_trasformations.py | 14 ++++++-------- 5 files changed, 18 insertions(+), 21 deletions(-) diff --git a/mypackage/point_cloud_generator.py b/mypackage/point_cloud_generator.py index 616e006..eedab58 100644 --- a/mypackage/point_cloud_generator.py +++ b/mypackage/point_cloud_generator.py @@ -121,7 +121,7 @@ def local_coordinate_system(self, relative_position): relative_position = np.clip(relative_position, 0, 1) origin = np.array([0, 1, 0]) * relative_position * self._length - return tf.CartesianCoordinateSystem3d(origin=origin) + return tf.CoordinateSystem(origin=origin) class RadialHorizontalTraceSegment: @@ -209,7 +209,7 @@ def local_coordinate_system(self, relative_position): translation = np.array([1, 0, 0]) * self._radius * self._sign_winding origin = np.matmul(basis, translation) - translation - return tf.CartesianCoordinateSystem3d(basis, origin) + return tf.CoordinateSystem(basis, origin) # Trace class ----------------------------------------------------------------- @@ -217,18 +217,17 @@ def local_coordinate_system(self, relative_position): class Trace: """Defines a 3d trace.""" - def __init__(self, segments, - coordinate_system=tf.CartesianCoordinateSystem3d()): + def __init__(self, segments, coordinate_system=tf.CoordinateSystem()): """ Constructor. :param segments: Single segment or list of segments :param coordinate_system: Coordinate system of the trace """ - if not isinstance(coordinate_system, tf.CartesianCoordinateSystem3d): + if not isinstance(coordinate_system, tf.CoordinateSystem): raise TypeError( "'coordinate_system' must be of type " - "transformations.CartesianCoordinateSystem3d") + "'transformations.CoordinateSystem'") self._segments = to_list(segments) self._create_lookups(coordinate_system) diff --git a/mypackage/transformations.py b/mypackage/transformations.py index 8194c1f..ec30e32 100644 --- a/mypackage/transformations.py +++ b/mypackage/transformations.py @@ -148,8 +148,8 @@ def change_of_basis_translation(ccs_from, ccs_to): # cartesian coordinate system class ------------------------------------------- -class CartesianCoordinateSystem3d: - """Defines a 3d cartesian coordinate system.""" +class CoordinateSystem: + """Defines a cartesian coordinate system in 3d.""" def __init__(self, basis=np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]]), origin=np.array([0, 0, 0])): @@ -201,7 +201,7 @@ def __add__(self, rhs_cs): """ basis = np.matmul(self.basis, rhs_cs.basis) origin = np.matmul(self.basis, rhs_cs.origin) + self.origin - return CartesianCoordinateSystem3d(basis, origin) + return CoordinateSystem(basis, origin) @classmethod def construct_from_orientation(cls, orientation, diff --git a/tests/_helpers.py b/tests/_helpers.py index e82066d..ba2b67e 100644 --- a/tests/_helpers.py +++ b/tests/_helpers.py @@ -31,4 +31,4 @@ def rotated_coordinate_system(angle_x=np.pi / 3, angle_y=np.pi / 4, rotated_basis = np.matmul(r_tot, basis) - return tf.CartesianCoordinateSystem3d(rotated_basis, np.array(origin)) + return tf.CoordinateSystem(rotated_basis, np.array(origin)) diff --git a/tests/test_point_cloud_generator.py b/tests/test_point_cloud_generator.py index ec4f23a..c084fc5 100644 --- a/tests/test_point_cloud_generator.py +++ b/tests/test_point_cloud_generator.py @@ -204,7 +204,7 @@ def default_trace_segment_tests(segment, tolerance_length=1E-9): lcs = segment.local_coordinate_system(0) # test that function actually returns a coordinate system class - assert isinstance(lcs, tf.CartesianCoordinateSystem3d) + assert isinstance(lcs, tf.CoordinateSystem) # check that origin for weight 0 is at [0,0,0] for i in range(3): @@ -331,7 +331,7 @@ def __init__(self): @staticmethod def local_coordinate_system(*args): - return tf.CartesianCoordinateSystem3d + return tf.CoordinateSystem custom_segment = CustomSegment() custom_segment.length = 3 @@ -378,7 +378,7 @@ def test_trace_local_coordinate_system(): # check with arbitrary coordinate system -------------- basis = tf.rotation_matrix_x(np.pi / 2) origin = np.array([-3, 2.5, 5]) - cs_base = tf.CartesianCoordinateSystem3d(basis, origin) + cs_base = tf.CoordinateSystem(basis, origin) trace = pcg.Trace([radial_segment, linear_segment], cs_base) diff --git a/tests/test_trasformations.py b/tests/test_trasformations.py index 53d4b82..78cf595 100644 --- a/tests/test_trasformations.py +++ b/tests/test_trasformations.py @@ -243,10 +243,8 @@ def test_change_of_basis_rotation(): base_from = rotated_positive_orthogonal_base(*angles_from) base_to = rotated_positive_orthogonal_base(*angles_to) - ccs_from = tf.CartesianCoordinateSystem3d(base_from, - random_non_unit_vector()) - ccs_to = tf.CartesianCoordinateSystem3d(base_to, - random_non_unit_vector()) + ccs_from = tf.CoordinateSystem(base_from, random_non_unit_vector()) + ccs_to = tf.CoordinateSystem(base_to, random_non_unit_vector()) matrix = tf.change_of_basis_rotation(ccs_from, ccs_to) @@ -260,8 +258,8 @@ def test_change_of_basis_translation(): base_from = rotated_positive_orthogonal_base(*random_non_unit_vector()) base_to = rotated_positive_orthogonal_base(*random_non_unit_vector()) - ccs_from = tf.CartesianCoordinateSystem3d(base_from, origin_from) - ccs_to = tf.CartesianCoordinateSystem3d(base_to, origin_to) + ccs_from = tf.CoordinateSystem(base_from, origin_from) + ccs_to = tf.CoordinateSystem(base_to, origin_to) diff = tf.change_of_basis_translation(ccs_from, ccs_to) @@ -274,7 +272,7 @@ def test_change_of_basis_translation(): def test_cartesian_coordinate_system_construction(): # alias name for class - name is too long :) - cls_ccs = tf.CartesianCoordinateSystem3d + cls_ccs = tf.CoordinateSystem # setup ----------------------------------------------- origin = [4, -2, 6] @@ -342,7 +340,7 @@ def test_cartesian_coordinate_system_construction(): def test_cartesian_coordinate_system_addition(): - cls_ccs = tf.CartesianCoordinateSystem3d + cls_ccs = tf.CoordinateSystem orientation0 = tf.rotation_matrix_z(np.pi / 2) origin0 = [1, 3, 2] From 35288715cdc0f1fdd787a5299269a5b6f6fc93d0 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Thu, 30 Jan 2020 11:40:14 +0100 Subject: [PATCH 124/177] Create internal utility package --- mypackage/_utility.py | 18 ++++++++++++++++++ mypackage/geometry.py | 22 +++------------------- mypackage/point_cloud_generator.py | 26 ++++---------------------- 3 files changed, 25 insertions(+), 41 deletions(-) create mode 100644 mypackage/_utility.py diff --git a/mypackage/_utility.py b/mypackage/_utility.py new file mode 100644 index 0000000..4fec225 --- /dev/null +++ b/mypackage/_utility.py @@ -0,0 +1,18 @@ +"""Contains package internal utility functions.""" + + +def to_list(var): + """ + Store the passed variable into a list and return it. + + If the variable is already a list, it is returned without modification. + If 'None' is passed, the function returns an empty list. + + :param var: Arbitrary variable + :return: List + """ + if isinstance(var, list): + return var + if var is None: + return [] + return [var] diff --git a/mypackage/geometry.py b/mypackage/geometry.py index 0081c1e..8ad639b 100644 --- a/mypackage/geometry.py +++ b/mypackage/geometry.py @@ -2,29 +2,13 @@ import numpy as np import math +import mypackage._utility as utils import mypackage.transformations as tf from scipy.spatial.transform import Rotation as R # Helper functions ------------------------------------------------------------ -def to_list(var): - """ - Store the passed variable into a list and return it. - - If the variable is already a list, it is returned without modification. - If 'None' is passed, the function returns an empty list. - - :param var: Arbitrary variable - :return: List - """ - if isinstance(var, list): - return var - if var is None: - return [] - return [var] - - def is_col_in_array(col, array): """ Check if a column (1d array) can be found inside of a 2d array. @@ -505,7 +489,7 @@ def __init__(self, segments=None): :param segments: Single segment or list of segments """ - self._segments = to_list(segments) + self._segments = utils.to_list(segments) @classmethod def interpolate(cls, shape_a, shape_b, weight, interpolation_schemes): @@ -577,7 +561,7 @@ def add_segments(self, segments): :param segments: Single segment or list of segments :return: --- """ - self._segments += to_list(segments) + self._segments += utils.to_list(segments) def apply_transformation(self, transformation_matrix): """ diff --git a/mypackage/point_cloud_generator.py b/mypackage/point_cloud_generator.py index eedab58..15bbbb7 100644 --- a/mypackage/point_cloud_generator.py +++ b/mypackage/point_cloud_generator.py @@ -1,29 +1,11 @@ """Contains methods and classes to generate 3d point clouds.""" import numpy as np +import mypackage._utility as utils import mypackage.geometry as geo import mypackage.transformations as tf -# Helper functions ------------------------------------------------------------ - -def to_list(var): - """ - Store the passed variable into a list and return it. - - If the variable is already a list, it is returned without modification. - If 'None' is passed, the function returns an empty list. - - :param var: Arbitrary variable - :return: List - """ - if isinstance(var, list): - return var - if var is None: - return [] - return [var] - - # Profile class --------------------------------------------------------------- class Profile: @@ -229,7 +211,7 @@ def __init__(self, segments, coordinate_system=tf.CoordinateSystem()): "'coordinate_system' must be of type " "'transformations.CoordinateSystem'") - self._segments = to_list(segments) + self._segments = utils.to_list(segments) self._create_lookups(coordinate_system) if self.length <= 0: @@ -375,8 +357,8 @@ def __init__(self, profiles, locations, interpolation_schemes): :param interpolation_schemes: List of interpolation schemes to define the interpolation between two locations. """ - locations = to_list(locations) - interpolation_schemes = to_list(interpolation_schemes) + locations = utils.to_list(locations) + interpolation_schemes = utils.to_list(interpolation_schemes) if not locations[0] == 0: locations = [0] + locations From 0f27fe75f17b297fc36e1066f3499d3a6e14093f Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Thu, 30 Jan 2020 15:53:23 +0100 Subject: [PATCH 125/177] Enforce connected segments in shape class --- mypackage/_utility.py | 20 ++++++++++++++++++++ mypackage/geometry.py | 26 ++++++++++++++++++++++++-- tests/test_geometry.py | 34 ++++++++++++++++++++++++++++------ tests/test_utility.py | 22 ++++++++++++++++++++++ 4 files changed, 94 insertions(+), 8 deletions(-) create mode 100644 tests/test_utility.py diff --git a/mypackage/_utility.py b/mypackage/_utility.py index 4fec225..2c5d4a3 100644 --- a/mypackage/_utility.py +++ b/mypackage/_utility.py @@ -1,5 +1,7 @@ """Contains package internal utility functions.""" +import math + def to_list(var): """ @@ -16,3 +18,21 @@ def to_list(var): if var is None: return [] return [var] + + +def vector_is_close(vec_a, vec_b, abs_tol=1E-9): + """ + Check if a vector is close or equal to another vector. + + :param vec_a: First vector + :param vec_b: Second vector + :param abs_tol: Absolute tolerance + :return: True or False + """ + if not vec_a.size == vec_b.size: + return False + for i in range(vec_a.size): + if not math.isclose(vec_a[i], vec_b[i], abs_tol=abs_tol): + return False + + return True diff --git a/mypackage/geometry.py b/mypackage/geometry.py index 8ad639b..46a0069 100644 --- a/mypackage/geometry.py +++ b/mypackage/geometry.py @@ -489,7 +489,25 @@ def __init__(self, segments=None): :param segments: Single segment or list of segments """ - self._segments = utils.to_list(segments) + segments = utils.to_list(segments) + self._check_segments_connected(segments) + self._segments = segments + + @staticmethod + def _check_segments_connected(segments): + """ + Check if all segments are connected to each other. + + The start point of a segment must be identical to the end point of + the previous segment. + + :param segments: List of segments + :return: --- + """ + for i in range(len(segments) - 1): + if not utils.vector_is_close(segments[i].point_end, + segments[i + 1].point_start): + raise Exception("Segments are not connected.") @classmethod def interpolate(cls, shape_a, shape_b, weight, interpolation_schemes): @@ -561,7 +579,11 @@ def add_segments(self, segments): :param segments: Single segment or list of segments :return: --- """ - self._segments += utils.to_list(segments) + segments = utils.to_list(segments) + if self.num_segments > 0: + self._check_segments_connected([self.segments[-1], segments[0]]) + self._check_segments_connected(segments) + self._segments += segments def apply_transformation(self, transformation_matrix): """ diff --git a/tests/test_geometry.py b/tests/test_geometry.py index b243d30..ba7ddb3 100644 --- a/tests/test_geometry.py +++ b/tests/test_geometry.py @@ -570,7 +570,7 @@ def test_arc_segment_interpolation(): # test Shape2d ---------------------------------------------------------------- def test_shape2d_construction(): - line_segment = geo.LineSegment.construct_from_points([0, 0], [1, 1]) + line_segment = geo.LineSegment.construct_from_points([1, 1], [1, 2]) arc_segment = geo.ArcSegment.construct_from_points([0, 0], [1, 1], [0, 1]) # Empty construction @@ -587,22 +587,44 @@ def test_shape2d_construction(): assert isinstance(shape.segments[0], geo.ArcSegment) assert isinstance(shape.segments[1], geo.LineSegment) + # exceptions ------------------------------------------ + + # segments not connected + with pytest.raises(Exception): + shape = geo.Shape2D([line_segment, arc_segment]) + def test_shape2d_segment_addition(): # Create shape and add segments - line_segment = geo.LineSegment.construct_from_points([0, 0], [1, 1]) + line_segment = geo.LineSegment.construct_from_points([1, 1], [0, 0]) arc_segment = geo.ArcSegment.construct_from_points([0, 0], [1, 1], [0, 1]) + arc_segment2 = geo.ArcSegment.construct_from_points([1, 1], [0, 0], [0, 1]) shape = geo.Shape2D() shape.add_segments(line_segment) assert shape.num_segments == 1 - shape.add_segments([arc_segment, arc_segment]) + shape.add_segments([arc_segment, arc_segment2]) assert shape.num_segments == 3 assert isinstance(shape.segments[0], geo.LineSegment) assert isinstance(shape.segments[1], geo.ArcSegment) assert isinstance(shape.segments[2], geo.ArcSegment) + # exceptions ------------------------------------------ + + # new segment are not connected to already included segments + with pytest.raises(Exception): + shape.add_segments(arc_segment2) + assert shape.num_segments == 3 # ensure shape is unmodified + + with pytest.raises(Exception): + shape.add_segments([arc_segment2, arc_segment]) + assert shape.num_segments == 3 # ensure shape is unmodified + + with pytest.raises(Exception): + shape.add_segments([arc_segment, arc_segment]) + assert shape.num_segments == 3 # ensure shape is unmodified + def test_shape2d_rasterization(): points = np.array([[0, 0], @@ -804,7 +826,7 @@ def test_shape2d_linear_interpolation(): shape_a = geo.Shape2D([segment_a0, segment_a1]) segment_b0 = geo.LineSegment.construct_from_points([1, 1], [2, -1]) - segment_b1 = geo.LineSegment.construct_from_points([4, -1], [3, 5]) + segment_b1 = geo.LineSegment.construct_from_points([2, -1], [3, 5]) shape_b = geo.Shape2D([segment_b0, segment_b1]) for i in range(5): @@ -817,7 +839,7 @@ def test_shape2d_linear_interpolation(): [1 + weight, 1 - 2 * weight]) helper.check_vectors_identical(shape_c.segments[1].point_start, - [1 + 3 * weight, 1 - 2 * weight]) + [1 + weight, 1 - 2 * weight]) helper.check_vectors_identical(shape_c.segments[1].point_end, [2 + weight, 5 * weight]) @@ -847,6 +869,6 @@ def test_shape2d_linear_interpolation(): # exceptions ------------------------------------------ - shape_a.add_segments([segment_a1]) + shape_a.add_segments(geo.LineSegment.construct_from_points([2, 0], [2, 2])) with pytest.raises(Exception): geo.Shape2D.linear_interpolation(shape_a, shape_b, 0.25) diff --git a/tests/test_utility.py b/tests/test_utility.py new file mode 100644 index 0000000..8932106 --- /dev/null +++ b/tests/test_utility.py @@ -0,0 +1,22 @@ +"""Test the internal utility functions.""" + +import numpy as np +import mypackage._utility as utils + + +def test_vector_is_close(): + vec_a = np.array([0, 1, 2]) + vec_b = np.array([3, 5, 1]) + + assert utils.vector_is_close(vec_a, vec_a) + assert utils.vector_is_close(vec_b, vec_b) + assert not utils.vector_is_close(vec_a, vec_b) + assert not utils.vector_is_close(vec_b, vec_a) + + # check tolerance + vec_c = vec_a + 0.0001 + assert utils.vector_is_close(vec_a, vec_c, abs_tol=0.00011) + assert not utils.vector_is_close(vec_a, vec_c, abs_tol=0.00009) + + # vectors have different size + assert not utils.vector_is_close(vec_a, vec_a[0:2]) From 44769443977ada628a7134fc4e9a5e4904ebe0ba Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Thu, 30 Jan 2020 16:17:59 +0100 Subject: [PATCH 126/177] Remove '2D' from shape class name --- mypackage/geometry.py | 4 +- mypackage/point_cloud_generator.py | 14 +++---- tests/test_geometry.py | 58 ++++++++++++++--------------- tests/test_point_cloud_generator.py | 44 +++++++++++----------- 4 files changed, 60 insertions(+), 60 deletions(-) diff --git a/mypackage/geometry.py b/mypackage/geometry.py index 46a0069..211329b 100644 --- a/mypackage/geometry.py +++ b/mypackage/geometry.py @@ -478,9 +478,9 @@ def translate(self, vector): self._points += np.ndarray((2, 1), float, np.array(vector, float)) -# Shape2d class --------------------------------------------------------------- +# Shape class ----------------------------------------------------------------- -class Shape2D: +class Shape: """Defines a shape in 2 dimensions.""" def __init__(self, segments=None): diff --git a/mypackage/point_cloud_generator.py b/mypackage/point_cloud_generator.py index 15bbbb7..dd42153 100644 --- a/mypackage/point_cloud_generator.py +++ b/mypackage/point_cloud_generator.py @@ -15,7 +15,7 @@ def __init__(self, shapes): """ Construct profile class. - :param: shapes: Instance or list of geo.Shape2D class(es) + :param: shapes: Instance or list of geo.Shape class(es) """ self._shapes = [] self.add_shapes(shapes) @@ -33,15 +33,15 @@ def add_shapes(self, shapes): """ Add shapes to the profile. - :param shapes: Instance or list of geo.Shape2D class(es) + :param shapes: Instance or list of geo.Shape class(es) :return: --- """ if not isinstance(shapes, list): shapes = [shapes] - if not all(isinstance(shape, geo.Shape2D) for shape in shapes): + if not all(isinstance(shape, geo.Shape) for shape in shapes): raise TypeError( - "Only instances or lists of Shape2d objects are accepted.") + "Only instances or lists of Shape objects are accepted.") self._shapes += shapes @@ -335,9 +335,9 @@ def interpolate(profile_a, profile_b, weight): shapes_c = [] for i in range(profile_a.num_shapes): - shapes_c += [geo.Shape2D.linear_interpolation(profile_a.shapes[i], - profile_b.shapes[i], - weight)] + shapes_c += [geo.Shape.linear_interpolation(profile_a.shapes[i], + profile_b.shapes[i], + weight)] return Profile(shapes_c) diff --git a/tests/test_geometry.py b/tests/test_geometry.py index ba7ddb3..84dd7bf 100644 --- a/tests/test_geometry.py +++ b/tests/test_geometry.py @@ -567,22 +567,22 @@ def test_arc_segment_interpolation(): test_arc_segment_interpolation() -# test Shape2d ---------------------------------------------------------------- +# test Shape ------------------------------------------------------------------ -def test_shape2d_construction(): +def test_shape_construction(): line_segment = geo.LineSegment.construct_from_points([1, 1], [1, 2]) arc_segment = geo.ArcSegment.construct_from_points([0, 0], [1, 1], [0, 1]) # Empty construction - shape = geo.Shape2D() + shape = geo.Shape() assert shape.num_segments == 0 # Single element construction shape - shape = geo.Shape2D(line_segment) + shape = geo.Shape(line_segment) assert shape.num_segments == 1 # Multi segment construction - shape = geo.Shape2D([arc_segment, line_segment]) + shape = geo.Shape([arc_segment, line_segment]) assert shape.num_segments == 2 assert isinstance(shape.segments[0], geo.ArcSegment) assert isinstance(shape.segments[1], geo.LineSegment) @@ -591,16 +591,16 @@ def test_shape2d_construction(): # segments not connected with pytest.raises(Exception): - shape = geo.Shape2D([line_segment, arc_segment]) + shape = geo.Shape([line_segment, arc_segment]) -def test_shape2d_segment_addition(): +def test_shape_segment_addition(): # Create shape and add segments line_segment = geo.LineSegment.construct_from_points([1, 1], [0, 0]) arc_segment = geo.ArcSegment.construct_from_points([0, 0], [1, 1], [0, 1]) arc_segment2 = geo.ArcSegment.construct_from_points([1, 1], [0, 0], [0, 1]) - shape = geo.Shape2D() + shape = geo.Shape() shape.add_segments(line_segment) assert shape.num_segments == 1 @@ -626,7 +626,7 @@ def test_shape2d_segment_addition(): assert shape.num_segments == 3 # ensure shape is unmodified -def test_shape2d_rasterization(): +def test_shape_rasterization(): points = np.array([[0, 0], [0, 1], [1, 1], @@ -634,7 +634,7 @@ def test_shape2d_rasterization(): raster_width = 0.2 - shape = geo.Shape2D( + shape = geo.Shape( geo.LineSegment.construct_from_points(points[0], points[1])) shape.add_segments( geo.LineSegment.construct_from_points(points[1], points[2])) @@ -657,10 +657,10 @@ def default_test_shape(): # create shape arc_segment = geo.ArcSegment.construct_from_points([3, 4], [5, 0], [6, 3]) line_segment = geo.LineSegment.construct_from_points([5, 0], [11, 3]) - return geo.Shape2D([arc_segment, line_segment]) + return geo.Shape([arc_segment, line_segment]) -def test_shape2d_translation(): +def test_shape_translation(): def check_point(point, point_ref, translation): helper.check_vectors_identical(point - translation, point_ref) @@ -694,7 +694,7 @@ def check_point(point, point_ref, translation): translation) -def test_shape2d_transformation(): +def test_shape_transformation(): # without reflection def check_point_rotation(point, point_ref): assert point[0] == point_ref[1] @@ -769,7 +769,7 @@ def check_reflected_point(point, reflected_point, axis_offset, assert np.abs(determinant) < 1E-8 -def shape2d_reflect_testcase(normal, distance_to_origin): +def shape_reflect_testcase(normal, distance_to_origin): direction_reflection_axis = np.array([normal[1], -normal[0]]) normal_length = np.linalg.norm(normal) unit_normal = np.array(normal) / normal_length @@ -810,28 +810,28 @@ def shape2d_reflect_testcase(normal, distance_to_origin): direction_reflection_axis) -def test_shape2d_reflect(): - shape2d_reflect_testcase([2, 1], np.linalg.norm([2, 1])) - shape2d_reflect_testcase([0, 1], 5) - shape2d_reflect_testcase([1, 0], 3) - shape2d_reflect_testcase([1, 0], -3) - shape2d_reflect_testcase([-7, 2], 4.12) - shape2d_reflect_testcase([-7, -2], 4.12) - shape2d_reflect_testcase([7, -2], 4.12) +def test_shape_reflect(): + shape_reflect_testcase([2, 1], np.linalg.norm([2, 1])) + shape_reflect_testcase([0, 1], 5) + shape_reflect_testcase([1, 0], 3) + shape_reflect_testcase([1, 0], -3) + shape_reflect_testcase([-7, 2], 4.12) + shape_reflect_testcase([-7, -2], 4.12) + shape_reflect_testcase([7, -2], 4.12) -def test_shape2d_linear_interpolation(): +def test_shape_linear_interpolation(): segment_a0 = geo.LineSegment.construct_from_points([0, 0], [1, 1]) segment_a1 = geo.LineSegment.construct_from_points([1, 1], [2, 0]) - shape_a = geo.Shape2D([segment_a0, segment_a1]) + shape_a = geo.Shape([segment_a0, segment_a1]) segment_b0 = geo.LineSegment.construct_from_points([1, 1], [2, -1]) segment_b1 = geo.LineSegment.construct_from_points([2, -1], [3, 5]) - shape_b = geo.Shape2D([segment_b0, segment_b1]) + shape_b = geo.Shape([segment_b0, segment_b1]) for i in range(5): weight = i / 4. - shape_c = geo.Shape2D.linear_interpolation(shape_a, shape_b, weight) + shape_c = geo.Shape.linear_interpolation(shape_a, shape_b, weight) helper.check_vectors_identical(shape_c.segments[0].point_start, [weight, weight]) @@ -845,7 +845,7 @@ def test_shape2d_linear_interpolation(): # check weight clipped to valid range ----------------- - shape_c = geo.Shape2D.linear_interpolation(shape_a, shape_b, -3) + shape_c = geo.Shape.linear_interpolation(shape_a, shape_b, -3) helper.check_vectors_identical(shape_c.segments[0].point_start, shape_a.segments[0].point_start) @@ -856,7 +856,7 @@ def test_shape2d_linear_interpolation(): helper.check_vectors_identical(shape_c.segments[1].point_end, shape_a.segments[1].point_end) - shape_c = geo.Shape2D.linear_interpolation(shape_a, shape_b, 100) + shape_c = geo.Shape.linear_interpolation(shape_a, shape_b, 100) helper.check_vectors_identical(shape_c.segments[0].point_start, shape_b.segments[0].point_start) @@ -871,4 +871,4 @@ def test_shape2d_linear_interpolation(): shape_a.add_segments(geo.LineSegment.construct_from_points([2, 0], [2, 2])) with pytest.raises(Exception): - geo.Shape2D.linear_interpolation(shape_a, shape_b, 0.25) + geo.Shape.linear_interpolation(shape_a, shape_b, 0.25) diff --git a/tests/test_point_cloud_generator.py b/tests/test_point_cloud_generator.py index c084fc5..b1ea681 100644 --- a/tests/test_point_cloud_generator.py +++ b/tests/test_point_cloud_generator.py @@ -50,15 +50,15 @@ def get_default_profiles(): a_0 = [0, 0] a_1 = [8, 16] a_2 = [16, 0] - shape_a01 = geo.Shape2D(geo.LineSegment.construct_from_points(a_0, a_1)) - shape_a12 = geo.Shape2D(geo.LineSegment.construct_from_points(a_1, a_2)) + shape_a01 = geo.Shape(geo.LineSegment.construct_from_points(a_0, a_1)) + shape_a12 = geo.Shape(geo.LineSegment.construct_from_points(a_1, a_2)) profile_a = pcg.Profile([shape_a01, shape_a12]) b_0 = [-4, 8] b_1 = [0, 8] b_2 = [16, -16] - shape_b01 = geo.Shape2D(geo.LineSegment.construct_from_points(b_0, b_1)) - shape_b12 = geo.Shape2D(geo.LineSegment.construct_from_points(b_1, b_2)) + shape_b01 = geo.Shape(geo.LineSegment.construct_from_points(b_0, b_1)) + shape_b12 = geo.Shape(geo.LineSegment.construct_from_points(b_1, b_2)) profile_b = pcg.Profile([shape_b01, shape_b12]) return [profile_a, profile_b] @@ -70,7 +70,7 @@ def test_profile_construction_and_shape_addition(): segment1 = geo.LineSegment.construct_from_points([1, 0], [2, -1]) segment2 = geo.LineSegment.construct_from_points([2, -1], [0, -1]) - shape = geo.Shape2D([segment0, segment1, segment2]) + shape = geo.Shape([segment0, segment1, segment2]) # Check invalid types with pytest.raises(TypeError): @@ -126,10 +126,10 @@ def test_profile_construction_and_shape_addition(): def test_profile_rasterization(): raster_width = 0.1 - shape0 = geo.Shape2D( + shape0 = geo.Shape( geo.LineSegment.construct_from_points([-1, 0], [-raster_width, 0])) - shape1 = geo.Shape2D(geo.LineSegment.construct_from_points([0, 0], [1, 0])) - shape2 = geo.Shape2D( + shape1 = geo.Shape(geo.LineSegment.construct_from_points([0, 0], [1, 0])) + shape2 = geo.Shape( geo.LineSegment.construct_from_points([1 + raster_width, 0], [2, 0])) profile = pcg.Profile([shape0, shape1]) @@ -428,15 +428,15 @@ def test_linear_profile_interpolation_sbs(): a_0 = [0, 0] a_1 = [8, 16] a_2 = [16, 0] - shape_a01 = geo.Shape2D(geo.LineSegment.construct_from_points(a_0, a_1)) - shape_a12 = geo.Shape2D(geo.LineSegment.construct_from_points(a_1, a_2)) + shape_a01 = geo.Shape(geo.LineSegment.construct_from_points(a_0, a_1)) + shape_a12 = geo.Shape(geo.LineSegment.construct_from_points(a_1, a_2)) profile_a = pcg.Profile([shape_a01, shape_a12]) b_0 = [-4, 8] b_1 = [0, 8] b_2 = [16, -16] - shape_b01 = geo.Shape2D(geo.LineSegment.construct_from_points(b_0, b_1)) - shape_b12 = geo.Shape2D(geo.LineSegment.construct_from_points(b_1, b_2)) + shape_b01 = geo.Shape(geo.LineSegment.construct_from_points(b_0, b_1)) + shape_b12 = geo.Shape(geo.LineSegment.construct_from_points(b_1, b_2)) profile_b = pcg.Profile([shape_b01, shape_b12]) [profile_a, profile_b] = get_default_profiles() @@ -474,8 +474,8 @@ def test_linear_profile_interpolation_sbs(): 0.5) # number of segments differ - shape_b012 = geo.Shape2D([geo.LineSegment.construct_from_points(b_0, b_1), - geo.LineSegment.construct_from_points(b_1, b_2)]) + shape_b012 = geo.Shape([geo.LineSegment.construct_from_points(b_0, b_1), + geo.LineSegment.construct_from_points(b_1, b_2)]) profile_b2 = pcg.Profile([shape_b01, shape_b012]) with pytest.raises(Exception): @@ -645,10 +645,10 @@ def test_geometry_rasterization_trace(): a3 = [1, 1] a4 = [1, 0] - shape_a012 = geo.Shape2D([geo.LineSegment.construct_from_points(a0, a1), - geo.LineSegment.construct_from_points(a1, a2)]) - shape_a234 = geo.Shape2D([geo.LineSegment.construct_from_points(a2, a3), - geo.LineSegment.construct_from_points(a3, a4)]) + shape_a012 = geo.Shape([geo.LineSegment.construct_from_points(a0, a1), + geo.LineSegment.construct_from_points(a1, a2)]) + shape_a234 = geo.Shape([geo.LineSegment.construct_from_points(a2, a3), + geo.LineSegment.construct_from_points(a3, a4)]) profile_a = pcg.Profile([shape_a012, shape_a234]) @@ -721,10 +721,10 @@ def test_geometry_rasterization_profile_interpolation(): a3 = [1, 1] a4 = [1, 0] - shape_a012 = geo.Shape2D([geo.LineSegment.construct_from_points(a0, a1), - geo.LineSegment.construct_from_points(a1, a2)]) - shape_a234 = geo.Shape2D([geo.LineSegment.construct_from_points(a2, a3), - geo.LineSegment.construct_from_points(a3, a4)]) + shape_a012 = geo.Shape([geo.LineSegment.construct_from_points(a0, a1), + geo.LineSegment.construct_from_points(a1, a2)]) + shape_a234 = geo.Shape([geo.LineSegment.construct_from_points(a2, a3), + geo.LineSegment.construct_from_points(a3, a4)]) shape_b012 = copy.deepcopy(shape_a012) shape_b234 = copy.deepcopy(shape_a234) From 6bbf4ebc182e39b50090c761100d32c60a64d24a Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Thu, 30 Jan 2020 16:27:52 +0100 Subject: [PATCH 127/177] Move some functions to utility package --- mypackage/_utility.py | 25 +++++++++++++++++++++++++ mypackage/geometry.py | 22 ---------------------- tests/test_geometry.py | 8 +++++--- 3 files changed, 30 insertions(+), 25 deletions(-) diff --git a/mypackage/_utility.py b/mypackage/_utility.py index 2c5d4a3..106ba09 100644 --- a/mypackage/_utility.py +++ b/mypackage/_utility.py @@ -1,6 +1,31 @@ """Contains package internal utility functions.""" import math +import numpy as np + + +def is_column_in_matrix(column, array): + """ + Check if a column (1d array) can be found inside of a 2d array. + + :param column: Column that should be checked + :param array: 2d array + :return: True or False + """ + return is_row_in_matrix(column, np.transpose(array)) + + +def is_row_in_matrix(row, array): + """ + Check if a row (1d array) can be found inside of a matrix. + + source: https://codereview.stackexchange.com/questions/193835 + + :param row: Row that should be checked + :param array: 2d array + :return: True or False + """ + return (array == row).all(axis=1).any() def to_list(var): diff --git a/mypackage/geometry.py b/mypackage/geometry.py index 211329b..f396ac9 100644 --- a/mypackage/geometry.py +++ b/mypackage/geometry.py @@ -9,28 +9,6 @@ # Helper functions ------------------------------------------------------------ -def is_col_in_array(col, array): - """ - Check if a column (1d array) can be found inside of a 2d array. - - :param row: Column that should be checked - :param array: 2d array - :return: True or False - """ - return is_row_in_array(col, np.transpose(array)) - - -# https://codereview.stackexchange.com/questions/193835 -def is_row_in_array(row, array): - """ - Check if a row (1d array) can be found inside of a 2d array. - - :param row: Row that should be checked - :param array: 2d array - :return: True or False - """ - return (array == row).all(axis=1).any() - def vector_points_to_left_of_vector(vector, vector_reference): """ diff --git a/tests/test_geometry.py b/tests/test_geometry.py index 84dd7bf..9f1b423 100644 --- a/tests/test_geometry.py +++ b/tests/test_geometry.py @@ -1,9 +1,11 @@ -import pytest +import mypackage._utility as utils import mypackage.geometry as geo +import tests._helpers as helper + +import pytest import numpy as np import math import copy -import tests._helpers as helper def test_vector_points_to_left_of_vector(): @@ -645,7 +647,7 @@ def test_shape_rasterization(): # Segment start and end points points must be included for point in points: - assert geo.is_col_in_array(point, data) + assert utils.is_column_in_matrix(point, data) # check effective raster width for i in range(1, data.shape[1]): From 695cada91e914a5040335a94ac41c38b544c40b5 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Thu, 30 Jan 2020 16:39:38 +0100 Subject: [PATCH 128/177] Rename and test utility functions --- mypackage/_utility.py | 16 +++++++++------- tests/test_utility.py | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 7 deletions(-) diff --git a/mypackage/_utility.py b/mypackage/_utility.py index 106ba09..11d4587 100644 --- a/mypackage/_utility.py +++ b/mypackage/_utility.py @@ -4,28 +4,30 @@ import numpy as np -def is_column_in_matrix(column, array): +def is_column_in_matrix(column, matrix): """ - Check if a column (1d array) can be found inside of a 2d array. + Check if a column (1d array) can be found inside of a matrix. :param column: Column that should be checked - :param array: 2d array + :param matrix: Matrix :return: True or False """ - return is_row_in_matrix(column, np.transpose(array)) + return is_row_in_matrix(column, np.transpose(matrix)) -def is_row_in_matrix(row, array): +def is_row_in_matrix(row, matrix): """ Check if a row (1d array) can be found inside of a matrix. source: https://codereview.stackexchange.com/questions/193835 :param row: Row that should be checked - :param array: 2d array + :param matrix: Matrix :return: True or False """ - return (array == row).all(axis=1).any() + if not matrix.shape[1] == np.array(row).size: + return False + return (matrix == row).all(axis=1).any() def to_list(var): diff --git a/tests/test_utility.py b/tests/test_utility.py index 8932106..b29f8b9 100644 --- a/tests/test_utility.py +++ b/tests/test_utility.py @@ -4,6 +4,40 @@ import mypackage._utility as utils +def test_is_column_in_matrix(): + c_0 = [1, 5, 2] + c_1 = [3, 2, 2] + c_2 = [1, 6, 1] + c_3 = [1, 6, 0] + matrix = np.array([c_0, c_1, c_2, c_3]).transpose() + + assert utils.is_column_in_matrix(c_0, matrix) + assert utils.is_column_in_matrix(c_1, matrix) + assert utils.is_column_in_matrix(c_2, matrix) + assert utils.is_column_in_matrix(c_3, matrix) + + assert not utils.is_column_in_matrix([1, 6], matrix) + assert not utils.is_column_in_matrix([1, 6, 2], matrix) + assert not utils.is_column_in_matrix([1, 1, 3, 1], matrix) + + +def test_is_row_in_matrix(): + c_0 = [1, 5, 2] + c_1 = [3, 2, 2] + c_2 = [1, 6, 1] + c_3 = [1, 6, 0] + matrix = np.array([c_0, c_1, c_2, c_3]) + + assert utils.is_row_in_matrix(c_0, matrix) + assert utils.is_row_in_matrix(c_1, matrix) + assert utils.is_row_in_matrix(c_2, matrix) + assert utils.is_row_in_matrix(c_3, matrix) + + assert not utils.is_row_in_matrix([1, 6], matrix) + assert not utils.is_row_in_matrix([1, 6, 2], matrix) + assert not utils.is_row_in_matrix([1, 1, 3, 1], matrix) + + def test_vector_is_close(): vec_a = np.array([0, 1, 2]) vec_b = np.array([3, 5, 1]) From b5d6a246650ecbb2215d5fbfbbc929cd0fa69816 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Thu, 30 Jan 2020 17:14:22 +0100 Subject: [PATCH 129/177] Fix some minor issues and docstrings --- mypackage/geometry.py | 6 +++--- mypackage/transformations.py | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/mypackage/geometry.py b/mypackage/geometry.py index f396ac9..3ba1b66 100644 --- a/mypackage/geometry.py +++ b/mypackage/geometry.py @@ -4,7 +4,7 @@ import math import mypackage._utility as utils import mypackage.transformations as tf -from scipy.spatial.transform import Rotation as R +from scipy.spatial.transform import Rotation as Rot # Helper functions ------------------------------------------------------------ @@ -309,7 +309,7 @@ def construct_from_points(cls, point_start, point_end, point_center, :param point_start: Starting point of the segment :param point_end: End point of the segment :param point_center: Center point of the arc - :param: arc_winding_ccw: Specifies if the arcs winding order is + :param arc_winding_ccw: Specifies if the arcs winding order is counter-clockwise :return: Arc segment """ @@ -440,7 +440,7 @@ def rasterize(self, raster_width, num_points_excluded_end=0): angles = np.arange(0, max_angle, self._sign_arc_winding * delta_angle) - rotation_matrices = R.from_euler('z', angles).as_dcm()[:, 0:2, 0:2] + rotation_matrices = Rot.from_euler('z', angles).as_dcm()[:, 0:2, 0:2] data = np.matmul(rotation_matrices, vec_center_start) + point_center diff --git a/mypackage/transformations.py b/mypackage/transformations.py index ec30e32..58cf245 100644 --- a/mypackage/transformations.py +++ b/mypackage/transformations.py @@ -244,7 +244,7 @@ def construct_from_xy_and_orientation(cls, vec_x, vec_y, :param origin: Position of the origin :return: Cartesian coordinate system """ - vec_z = cls._calcualte_orthogonal_axis(vec_x, + vec_z = cls._calculate_orthogonal_axis(vec_x, vec_y) * cls._sign_orientation( positive_orientation) @@ -265,7 +265,7 @@ def construct_from_yz_and_orientation(cls, vec_y, vec_z, :param origin: Position of the origin :return: Cartesian coordinate system """ - vec_x = cls._calcualte_orthogonal_axis(vec_y, + vec_x = cls._calculate_orthogonal_axis(vec_y, vec_z) * cls._sign_orientation( positive_orientation) @@ -286,7 +286,7 @@ def construct_from_xz_and_orientation(cls, vec_x, vec_z, :param origin: Position of the origin :return: Cartesian coordinate system """ - vec_y = cls._calcualte_orthogonal_axis(vec_z, + vec_y = cls._calculate_orthogonal_axis(vec_z, vec_x) * cls._sign_orientation( positive_orientation) @@ -308,15 +308,15 @@ def _sign_orientation(positive_orientation): return -1 @staticmethod - def _calcualte_orthogonal_axis(a_0, a_1): + def _calculate_orthogonal_axis(a_0, a_1): """ Calculate an axis which is orthogonal to two other axes. The calculated axis has a positive orientation towards the other 2 axes. - :param a0: First axis - :param a1: Second axis + :param a_0: First axis + :param a_1: Second axis :return: Orthogonal axis """ return np.cross(a_0, a_1) From 4f8ed6f6ebf7029c72ae3414bc6eb61d15e144cb Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Fri, 31 Jan 2020 14:49:57 +0100 Subject: [PATCH 130/177] Add and test arc segment construction with radius --- mypackage/_utility.py | 10 ++++ mypackage/geometry.py | 48 +++++++++++++++--- tests/test_geometry.py | 107 +++++++++++++++++++++++++++++------------ 3 files changed, 129 insertions(+), 36 deletions(-) diff --git a/mypackage/_utility.py b/mypackage/_utility.py index 11d4587..a7dd591 100644 --- a/mypackage/_utility.py +++ b/mypackage/_utility.py @@ -30,6 +30,16 @@ def is_row_in_matrix(row, matrix): return (matrix == row).all(axis=1).any() +def to_float_array(container): + """ + Cast the passed container to a numpy array of floats. + + :param container: Container which can be cast to a numpy array + :return: + """ + return np.array(container, dtype=float) + + def to_list(var): """ Store the passed variable into a list and return it. diff --git a/mypackage/geometry.py b/mypackage/geometry.py index 3ba1b66..138f7df 100644 --- a/mypackage/geometry.py +++ b/mypackage/geometry.py @@ -2,7 +2,7 @@ import numpy as np import math -import mypackage._utility as utils +import mypackage._utility as ut import mypackage.transformations as tf from scipy.spatial.transform import Rotation as Rot @@ -304,7 +304,7 @@ def _check_valid(self): def construct_from_points(cls, point_start, point_end, point_center, arc_winding_ccw=True): """ - Construct an arc segment from three points. + Construct an arc segment from three points (start, end, center). :param point_start: Starting point of the segment :param point_end: End point of the segment @@ -317,6 +317,42 @@ def construct_from_points(cls, point_start, point_end, point_center, np.array([point_start, point_end, point_center], dtype=float)) return cls(points, arc_winding_ccw) + @classmethod + def construct_with_radius(cls, point_start, point_end, radius, + center_left_of_line=True, arc_winding_ccw=True): + """ + Construct an arc segment with a radius and the start and end points. + + :param point_start: Starting point of the segment + :param point_end: End point of the segment + :param radius: Radius + :param center_left_of_line: Specifies if the center point is located + to the left of the vectpr point_start -> point_end + :param arc_winding_ccw: Specifies if the arcs winding order is + counter-clockwise + :return: Arc segment + """ + point_start = ut.to_float_array(point_start) + point_end = ut.to_float_array(point_end) + + vec_start_end = point_end - point_start + if center_left_of_line: + vec_normal = np.array([-vec_start_end[1], vec_start_end[0]]) + else: + vec_normal = np.array([vec_start_end[1], -vec_start_end[0]]) + + squared_length = np.dot(vec_start_end, vec_start_end) + squared_radius = radius * radius + + normal_scaling = np.sqrt( + np.clip(squared_radius / squared_length - 0.25, 0, None)) + + vec_start_center = 0.5 * vec_start_end + vec_normal * normal_scaling + point_center = point_start + vec_start_center + + return cls.construct_from_points(point_start, point_end, point_center, + arc_winding_ccw) + @classmethod def linear_interpolation(cls, segment_a, segment_b, weight): """ @@ -467,7 +503,7 @@ def __init__(self, segments=None): :param segments: Single segment or list of segments """ - segments = utils.to_list(segments) + segments = ut.to_list(segments) self._check_segments_connected(segments) self._segments = segments @@ -483,8 +519,8 @@ def _check_segments_connected(segments): :return: --- """ for i in range(len(segments) - 1): - if not utils.vector_is_close(segments[i].point_end, - segments[i + 1].point_start): + if not ut.vector_is_close(segments[i].point_end, + segments[i + 1].point_start): raise Exception("Segments are not connected.") @classmethod @@ -557,7 +593,7 @@ def add_segments(self, segments): :param segments: Single segment or list of segments :return: --- """ - segments = utils.to_list(segments) + segments = ut.to_list(segments) if self.num_segments > 0: self._check_segments_connected([self.segments[-1], segments[0]]) self._check_segments_connected(segments) diff --git a/tests/test_geometry.py b/tests/test_geometry.py index 9f1b423..89fa3b3 100644 --- a/tests/test_geometry.py +++ b/tests/test_geometry.py @@ -262,6 +262,18 @@ def test_line_segment_interpolation(): # test ArcSegment ------------------------------------------------------------ +def check_arc_segment_values(segment, point_start, point_end, point_center, + winding_ccw, radius, arc_angle, arc_length): + helper.check_vectors_identical(segment.point_start, point_start) + helper.check_vectors_identical(segment.point_end, point_end) + helper.check_vectors_identical(segment.point_center, point_center) + + assert segment.is_arc_winding_ccw() is winding_ccw + assert math.isclose(segment.radius, radius) + assert math.isclose(segment.arc_angle, arc_angle) + assert math.isclose(segment.arc_length, arc_length) + + def arc_segment_test(point_center, point_start, point_end, raster_width, arc_winding_ccw, check_winding): point_center = np.array(point_center) @@ -339,24 +351,75 @@ def test_arc_segment_construction(): with pytest.raises(ValueError): geo.ArcSegment([[[3, 5], [3, 4]]]) - # factories ------------------------------------------- - - segment_cw = geo.ArcSegment.construct_from_points([3, 3], [6, 6], [6, 3], - False) - segment_ccw = geo.ArcSegment.construct_from_points([3, 3], [6, 6], [6, 3], - True) - assert not segment_cw.is_arc_winding_ccw() - assert segment_ccw.is_arc_winding_ccw() +def test_arc_segment_factories(): + # construction with center point ---------------------- + point_start = [3, 3] + point_end = [6, 6] + point_center_left = [3, 6] + point_center_right = [6, 3] - assert math.isclose(segment_cw.radius, 3) - assert math.isclose(segment_ccw.radius, 3) + # expected results + radius = 3 + angle_small = np.pi * 0.5 + angle_large = np.pi * 1.5 + arc_length_small = np.pi * 1.5 + arc_length_large = np.pi * 4.5 - assert math.isclose(segment_cw.arc_angle, 1 / 2 * np.pi) - assert math.isclose(segment_ccw.arc_angle, 3 / 2 * np.pi) + segment_cw = geo.ArcSegment.construct_from_points(point_start, point_end, + point_center_right, + False) + segment_ccw = geo.ArcSegment.construct_from_points(point_start, point_end, + point_center_right, + True) - assert math.isclose(segment_cw.arc_length, 3 / 2 * np.pi) - assert math.isclose(segment_ccw.arc_length, 9 / 2 * np.pi) + check_arc_segment_values(segment_cw, point_start, point_end, + point_center_right, False, radius, angle_small, + arc_length_small) + check_arc_segment_values(segment_ccw, point_start, point_end, + point_center_right, True, radius, angle_large, + arc_length_large) + + # construction with radius ---------------------- + + # center left of line + segment_cw = geo.ArcSegment.construct_with_radius(point_start, point_end, + radius, True, False) + segment_ccw = geo.ArcSegment.construct_with_radius(point_start, point_end, + radius, True, True) + + check_arc_segment_values(segment_cw, point_start, point_end, + point_center_left, False, radius, angle_large, + arc_length_large) + check_arc_segment_values(segment_ccw, point_start, point_end, + point_center_left, True, radius, angle_small, + arc_length_small) + + # center right of line + segment_cw = geo.ArcSegment.construct_with_radius(point_start, point_end, + radius, False, False) + segment_ccw = geo.ArcSegment.construct_with_radius(point_start, point_end, + radius, False, True) + + check_arc_segment_values(segment_cw, point_start, point_end, + point_center_right, False, radius, angle_small, + arc_length_small) + check_arc_segment_values(segment_ccw, point_start, point_end, + point_center_right, True, radius, angle_large, + arc_length_large) + + # check that too small radii will be clipped to minimal radius + segment_cw = geo.ArcSegment.construct_with_radius(point_start, point_end, + 0.1, False, False) + segment_ccw = geo.ArcSegment.construct_with_radius(point_start, point_end, + 0.1, False, True) + + check_arc_segment_values(segment_cw, point_start, point_end, [4.5, 4.5], + False, np.sqrt(18) / 2, np.pi, + np.pi * np.sqrt(18) / 2) + check_arc_segment_values(segment_ccw, point_start, point_end, [4.5, 4.5], + True, np.sqrt(18) / 2, np.pi, + np.pi * np.sqrt(18) / 2) def test_arc_segment_rasterization(): @@ -412,19 +475,6 @@ def not_above_center(p, c): not_above_center) -def check_arc_segment_values(segment, point_start, point_end, point_center, - winding_ccw, radius, arc_angle, arc_length): - helper.check_vectors_identical(segment.point_start, np.array(point_start)) - helper.check_vectors_identical(segment.point_end, np.array(point_end)) - helper.check_vectors_identical(segment.point_center, - np.array(point_center)) - - assert segment.is_arc_winding_ccw() is winding_ccw - assert math.isclose(segment.radius, radius) - assert math.isclose(segment.arc_angle, arc_angle) - assert math.isclose(segment.arc_length, arc_length) - - def test_arc_segment_transformations(): # translation ----------------------------------------- segment_cw = geo.ArcSegment.construct_from_points([3, 3], [5, 5], [5, 3], @@ -566,9 +616,6 @@ def test_arc_segment_interpolation(): geo.ArcSegment.linear_interpolation(segment_a, segment_b, 1) -test_arc_segment_interpolation() - - # test Shape ------------------------------------------------------------------ def test_shape_construction(): From 20d0a10c3f4107750a4a65a22cb408f95d419fb0 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Mon, 3 Feb 2020 14:46:00 +0100 Subject: [PATCH 131/177] Test general shape interpolation --- mypackage/geometry.py | 54 +++++++--- tests/test_geometry.py | 158 ++++++++++++++++++---------- tests/test_point_cloud_generator.py | 18 ++-- 3 files changed, 149 insertions(+), 81 deletions(-) diff --git a/mypackage/geometry.py b/mypackage/geometry.py index 138f7df..d332172 100644 --- a/mypackage/geometry.py +++ b/mypackage/geometry.py @@ -327,7 +327,7 @@ def construct_with_radius(cls, point_start, point_end, radius, :param point_end: End point of the segment :param radius: Radius :param center_left_of_line: Specifies if the center point is located - to the left of the vectpr point_start -> point_end + to the left of the vector point_start -> point_end :param arc_winding_ccw: Specifies if the arcs winding order is counter-clockwise :return: Arc segment @@ -358,18 +358,29 @@ def linear_interpolation(cls, segment_a, segment_b, weight): """ Interpolate two arc segments linearly. + This function is not implemented, since linear interpolation of an + arc segment is not unique. The 'Shape' class requires succeeding + segments to be connected through a common point. Therefore two + connected segments must interpolate the connecting point in the same + way. Connecting an arc segment to two line segments would enforce a + linear interpolation of the start and end points. If the centre + point is also interpolated in a linear way, might (or might not) + result in different distances of start and end point to the center, + which invalidates the arc segment. Alternatively, one can + interpolate the radius linearly which guarantees a valid arc + segment, but this can cause the center point to vary even though it + is the same in both interpolated segments. To ensure the desired + interpolation behavior, you have to provide a custom interpolation. + :param segment_a: First segment :param segment_b: Second segment :param weight: Weighting factor in the range [0 .. 1] where 0 is segment a and 1 is segment b :return: Interpolated segment """ - # implementation ->segment start and end have to be interpolated - # linearly. Otherwise there might occur gaps in interpolated shapes - # at the connecting segment points --- the center point has to be - # determined automatically. 2 ways -> linear angle interpolation or - # linear radius interpolation - raise Exception("Not implemented.") + raise Exception( + "Linear interpolation of an arc segment is not unique (see " + "doctstring). You need to provide a custom interpolation.") @property def arc_angle(self): @@ -389,6 +400,15 @@ def arc_length(self): """ return self._arc_length + @property + def arc_winding_ccw(self): + """ + Get True if the winding order is counter-clockwise. False if clockwise. + + :return: True or False + """ + return self._sign_arc_winding > 0 + @property def point_center(self): """ @@ -416,6 +436,18 @@ def point_start(self): """ return self._points[:, 0] + @property + def points(self): + """ + Get the segments points in form of a 2x3 matrix. + + The first column represents the starting point, the second one the + end and the third one the center. + + :return: 2x3 matrix containing the segments points + """ + return self._points + @property def radius(self): """ @@ -436,14 +468,6 @@ def apply_transformation(self, matrix): self._sign_arc_winding *= reflection_sign(matrix) self._calculate_arc_parameters() - def is_arc_winding_ccw(self): - """ - Get True if the winding order is counter-clockwise. False if clockwise. - - :return: True or False - """ - return self._sign_arc_winding > 0 - def rasterize(self, raster_width, num_points_excluded_end=0): """ Create an array of points that describe the segments contour. diff --git a/tests/test_geometry.py b/tests/test_geometry.py index 89fa3b3..db9dd89 100644 --- a/tests/test_geometry.py +++ b/tests/test_geometry.py @@ -1,6 +1,6 @@ import mypackage._utility as utils import mypackage.geometry as geo -import tests._helpers as helper +import tests._helpers as helpers import pytest import numpy as np @@ -8,6 +8,17 @@ import copy +# helpers --------------------------------------------------------------------- + +def check_segments_identical(a, b): + assert isinstance(a, type(b)) + helpers.check_vectors_identical(a.point_start, b.point_start) + helpers.check_vectors_identical(a.point_end, b.point_end) + if isinstance(a, geo.ArcSegment): + assert a.arc_winding_ccw == b.arc_winding_ccw + helpers.check_vectors_identical(a.point_center, b.point_center) + + def test_vector_points_to_left_of_vector(): assert geo.vector_points_to_left_of_vector([-0.1, 1], [0, 1]) > 0 assert geo.vector_points_to_left_of_vector([-0.1, -1], [0, 1]) > 0 @@ -83,8 +94,8 @@ def default_segment_rasterization_tests(segment, raster_width, point_start, # Check if first and last point of the data are identical to the segment # start and end - helper.check_vectors_identical(data[:, 0], point_start) - helper.check_vectors_identical(data[:, -1], point_end) + helpers.check_vectors_identical(data[:, 0], point_start) + helpers.check_vectors_identical(data[:, -1], point_end) for i in range(num_points - 1): point = data[:, i] @@ -101,15 +112,15 @@ def default_segment_rasterization_tests(segment, raster_width, point_start, assert num_points - 2 == num_points_m2 for i in range(num_points_m2): - helper.check_vectors_identical(data[:, i], data_m2[:, i]) + helpers.check_vectors_identical(data[:, i], data_m2[:, i]) # check that rasterization with to large raster width still works data_200 = segment.rasterize(200) num_points_200 = data_200.shape[1] assert num_points_200 == 2 - helper.check_vectors_identical(point_start, data_200[:, 0]) - helper.check_vectors_identical(point_end, data_200[:, 1]) + helpers.check_vectors_identical(point_start, data_200[:, 0]) + helpers.check_vectors_identical(point_end, data_200[:, 1]) # check exceptions with pytest.raises(ValueError): @@ -173,8 +184,8 @@ def test_line_segment_transformations(): segment = geo.LineSegment.construct_from_points([3, 3], [4, 5]) segment.translate([-1, 4]) - helper.check_vectors_identical(segment.point_start, np.array([2, 7])) - helper.check_vectors_identical(segment.point_end, np.array([3, 9])) + helpers.check_vectors_identical(segment.point_start, np.array([2, 7])) + helpers.check_vectors_identical(segment.point_end, np.array([3, 9])) assert math.isclose(segment.length, np.sqrt(5)) # 45 degree rotation ---------------------------------- @@ -188,8 +199,8 @@ def test_line_segment_transformations(): exp_start = [0, np.sqrt(8)] exp_end = np.matmul(rotation_matrix, [3, 6]) - helper.check_vectors_identical(segment.point_start, exp_start) - helper.check_vectors_identical(segment.point_end, exp_end) + helpers.check_vectors_identical(segment.point_start, exp_start) + helpers.check_vectors_identical(segment.point_end, exp_end) assert math.isclose(segment.length, np.sqrt(17)) # reflection at 45 degree line ------------------------ @@ -199,8 +210,8 @@ def test_line_segment_transformations(): segment = geo.LineSegment.construct_from_points([-1, 3], [6, 1]) segment.apply_transformation(reflection_matrix) - helper.check_vectors_identical(segment.point_start, [3, -1]) - helper.check_vectors_identical(segment.point_end, [1, 6]) + helpers.check_vectors_identical(segment.point_start, [3, -1]) + helpers.check_vectors_identical(segment.point_end, [1, 6]) assert math.isclose(segment.length, np.sqrt(53)) # scaling --------------------------------------------- @@ -209,8 +220,8 @@ def test_line_segment_transformations(): segment = geo.LineSegment.construct_from_points([-2, 2], [1, 4]) segment.apply_transformation(scale_matrix) - helper.check_vectors_identical(segment.point_start, [-8, 1]) - helper.check_vectors_identical(segment.point_end, [4, 2]) + helpers.check_vectors_identical(segment.point_start, [-8, 1]) + helpers.check_vectors_identical(segment.point_end, [4, 2]) # length changes due to scaling! assert math.isclose(segment.length, np.sqrt(145)) @@ -239,14 +250,14 @@ def test_line_segment_interpolation(): # check weight clipped to valid range ----------------- segment_c = geo.LineSegment.linear_interpolation(segment_a, segment_b, -3) - helper.check_vectors_identical(segment_c.point_start, - segment_a.point_start) - helper.check_vectors_identical(segment_c.point_end, segment_a.point_end) + helpers.check_vectors_identical(segment_c.point_start, + segment_a.point_start) + helpers.check_vectors_identical(segment_c.point_end, segment_a.point_end) segment_c = geo.LineSegment.linear_interpolation(segment_a, segment_b, 6) - helper.check_vectors_identical(segment_c.point_start, - segment_b.point_start) - helper.check_vectors_identical(segment_c.point_end, segment_b.point_end) + helpers.check_vectors_identical(segment_c.point_start, + segment_b.point_start) + helpers.check_vectors_identical(segment_c.point_end, segment_b.point_end) # exceptions ------------------------------------------ @@ -264,11 +275,11 @@ def test_line_segment_interpolation(): def check_arc_segment_values(segment, point_start, point_end, point_center, winding_ccw, radius, arc_angle, arc_length): - helper.check_vectors_identical(segment.point_start, point_start) - helper.check_vectors_identical(segment.point_end, point_end) - helper.check_vectors_identical(segment.point_center, point_center) + helpers.check_vectors_identical(segment.point_start, point_start) + helpers.check_vectors_identical(segment.point_end, point_end) + helpers.check_vectors_identical(segment.point_center, point_center) - assert segment.is_arc_winding_ccw() is winding_ccw + assert segment.arc_winding_ccw is winding_ccw assert math.isclose(segment.radius, radius) assert math.isclose(segment.arc_angle, arc_angle) assert math.isclose(segment.arc_length, arc_length) @@ -310,8 +321,8 @@ def test_arc_segment_construction(): segment_cw = geo.ArcSegment(points, False) segment_ccw = geo.ArcSegment(points, True) - assert not segment_cw.is_arc_winding_ccw() - assert segment_ccw.is_arc_winding_ccw() + assert not segment_cw.arc_winding_ccw + assert segment_ccw.arc_winding_ccw assert math.isclose(segment_cw.radius, 3) assert math.isclose(segment_ccw.radius, 3) @@ -609,7 +620,7 @@ def test_arc_segment_transformations(): def test_arc_segment_interpolation(): segment_a = geo.ArcSegment.construct_from_points([0, 0], [1, 1], [1, 0]) - segment_b = geo.ArcSegment.construct_from_points([0, 0], [1, 1], [1, 0]) + segment_b = geo.ArcSegment.construct_from_points([0, 0], [2, 2], [0, 2]) # not implemented yet with pytest.raises(Exception): @@ -711,7 +722,7 @@ def default_test_shape(): def test_shape_translation(): def check_point(point, point_ref, translation): - helper.check_vectors_identical(point - translation, point_ref) + helpers.check_vectors_identical(point - translation, point_ref) translation = [3, 4] @@ -724,8 +735,7 @@ def check_point(point, point_ref, translation): arc_segment = shape._segments[0] arc_segment_ref = shape_ref._segments[0] - assert (arc_segment.is_arc_winding_ccw() == - arc_segment_ref.is_arc_winding_ccw()) + assert (arc_segment.arc_winding_ccw == arc_segment_ref.arc_winding_ccw) check_point(arc_segment.point_start, arc_segment_ref.point_start, translation) @@ -760,8 +770,7 @@ def check_point_rotation(point, point_ref): arc_segment = shape.segments[0] arc_segment_ref = shape_ref.segments[0] - assert (arc_segment.is_arc_winding_ccw() == - arc_segment_ref.is_arc_winding_ccw()) + assert (arc_segment.arc_winding_ccw == arc_segment_ref.arc_winding_ccw) check_point_rotation(arc_segment.point_start, arc_segment_ref.point_start) check_point_rotation(arc_segment.point_end, arc_segment_ref.point_end) @@ -790,8 +799,7 @@ def check_point_reflection(point, point_ref): arc_segment = shape.segments[0] arc_segment_ref = shape_ref.segments[0] - assert (arc_segment.is_arc_winding_ccw() != - arc_segment_ref.is_arc_winding_ccw()) + assert (arc_segment.arc_winding_ccw != arc_segment_ref.arc_winding_ccw) check_point_reflection(arc_segment.point_start, arc_segment_ref.point_start) @@ -869,6 +877,40 @@ def test_shape_reflect(): shape_reflect_testcase([7, -2], 4.12) +def interpolation_nearest(segment_a, segment_b, weight): + if weight > 0.5: + return segment_b + else: + return segment_a + + +def test_shape_interpolation_general(): + segment_a0 = geo.LineSegment.construct_from_points([-1, -1], [1, 1]) + segment_a1 = geo.LineSegment.construct_from_points([1, 1], [3, -1]) + shape_a = geo.Shape([segment_a0, segment_a1]) + + segment_b0 = geo.LineSegment.construct_from_points([-1, 4], [1, 1]) + segment_b1 = geo.LineSegment.construct_from_points([1, 1], [3, 4]) + shape_b = geo.Shape([segment_b0, segment_b1]) + + interpolations = [geo.LineSegment.linear_interpolation, + interpolation_nearest] + for i in range(6): + weight = i / 5. + shape_c = geo.Shape.interpolate(shape_a, shape_b, weight, + interpolations) + assert shape_c.num_segments == 2 + + exp_segment_c0 = geo.LineSegment.construct_from_points( + [-1, -1 + 5 * weight], [1, 1]) + check_segments_identical(shape_c.segments[0], exp_segment_c0) + + if weight > 0.5: + check_segments_identical(shape_c.segments[1], segment_b1) + else: + check_segments_identical(shape_c.segments[1], segment_a1) + + def test_shape_linear_interpolation(): segment_a0 = geo.LineSegment.construct_from_points([0, 0], [1, 1]) segment_a1 = geo.LineSegment.construct_from_points([1, 1], [2, 0]) @@ -882,39 +924,39 @@ def test_shape_linear_interpolation(): weight = i / 4. shape_c = geo.Shape.linear_interpolation(shape_a, shape_b, weight) - helper.check_vectors_identical(shape_c.segments[0].point_start, - [weight, weight]) - helper.check_vectors_identical(shape_c.segments[0].point_end, - [1 + weight, 1 - 2 * weight]) + helpers.check_vectors_identical(shape_c.segments[0].point_start, + [weight, weight]) + helpers.check_vectors_identical(shape_c.segments[0].point_end, + [1 + weight, 1 - 2 * weight]) - helper.check_vectors_identical(shape_c.segments[1].point_start, - [1 + weight, 1 - 2 * weight]) - helper.check_vectors_identical(shape_c.segments[1].point_end, - [2 + weight, 5 * weight]) + helpers.check_vectors_identical(shape_c.segments[1].point_start, + [1 + weight, 1 - 2 * weight]) + helpers.check_vectors_identical(shape_c.segments[1].point_end, + [2 + weight, 5 * weight]) # check weight clipped to valid range ----------------- shape_c = geo.Shape.linear_interpolation(shape_a, shape_b, -3) - helper.check_vectors_identical(shape_c.segments[0].point_start, - shape_a.segments[0].point_start) - helper.check_vectors_identical(shape_c.segments[0].point_end, - shape_a.segments[0].point_end) - helper.check_vectors_identical(shape_c.segments[1].point_start, - shape_a.segments[1].point_start) - helper.check_vectors_identical(shape_c.segments[1].point_end, - shape_a.segments[1].point_end) + helpers.check_vectors_identical(shape_c.segments[0].point_start, + shape_a.segments[0].point_start) + helpers.check_vectors_identical(shape_c.segments[0].point_end, + shape_a.segments[0].point_end) + helpers.check_vectors_identical(shape_c.segments[1].point_start, + shape_a.segments[1].point_start) + helpers.check_vectors_identical(shape_c.segments[1].point_end, + shape_a.segments[1].point_end) shape_c = geo.Shape.linear_interpolation(shape_a, shape_b, 100) - helper.check_vectors_identical(shape_c.segments[0].point_start, - shape_b.segments[0].point_start) - helper.check_vectors_identical(shape_c.segments[0].point_end, - shape_b.segments[0].point_end) - helper.check_vectors_identical(shape_c.segments[1].point_start, - shape_b.segments[1].point_start) - helper.check_vectors_identical(shape_c.segments[1].point_end, - shape_b.segments[1].point_end) + helpers.check_vectors_identical(shape_c.segments[0].point_start, + shape_b.segments[0].point_start) + helpers.check_vectors_identical(shape_c.segments[0].point_end, + shape_b.segments[0].point_end) + helpers.check_vectors_identical(shape_c.segments[1].point_start, + shape_b.segments[1].point_start) + helpers.check_vectors_identical(shape_c.segments[1].point_end, + shape_b.segments[1].point_end) # exceptions ------------------------------------------ diff --git a/tests/test_point_cloud_generator.py b/tests/test_point_cloud_generator.py index b1ea681..bab3fb3 100644 --- a/tests/test_point_cloud_generator.py +++ b/tests/test_point_cloud_generator.py @@ -16,17 +16,19 @@ def check_profiles_identical(a, b): check_shapes_identical(a.shapes[i], b.shapes[i]) +def check_segments_identical(a, b): + assert isinstance(a, type(b)) + helpers.check_vectors_identical(a.point_start, b.point_start) + helpers.check_vectors_identical(a.point_end, b.point_end) + if isinstance(a, geo.ArcSegment): + assert a.arc_winding_ccw == b.arc_winding_ccw + helpers.check_vectors_identical(a.point_center, b.point_center) + + def check_shapes_identical(a, b): assert a.num_segments == b.num_segments for i in range(a.num_segments): - assert isinstance(a.segments[i], type(b.segments[i])) - helpers.check_vectors_identical(a.segments[i].point_start, - b.segments[i].point_start) - helpers.check_vectors_identical(a.segments[i].point_end, - b.segments[i].point_end) - if isinstance(a.segments[i], geo.ArcSegment): - helpers.check_vectors_identical(a.segments[i].point_center, - b.segments[i].point_center) + check_segments_identical(a.segments[i], b.segments[i]) def check_trace_segments_identical(a, b): From 0ed1b946dcfa558620f88c0063a2eba033701bdd Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Mon, 3 Feb 2020 14:52:38 +0100 Subject: [PATCH 132/177] Rename some functions --- mypackage/geometry.py | 10 ++-- tests/test_geometry.py | 92 ++++++++++++++--------------- tests/test_point_cloud_generator.py | 48 +++++++-------- 3 files changed, 75 insertions(+), 75 deletions(-) diff --git a/mypackage/geometry.py b/mypackage/geometry.py index d332172..25a860a 100644 --- a/mypackage/geometry.py +++ b/mypackage/geometry.py @@ -92,9 +92,9 @@ def _calculate_length(self): raise ValueError("Segment length is 0.") @classmethod - def construct_from_points(cls, point_start, point_end): + def construct_with_points(cls, point_start, point_end): """ - Construct a line segment from two points. + Construct a line segment with two points. :param point_start: Starting point of the segment :param point_end: End point of the segment @@ -301,10 +301,10 @@ def _check_valid(self): raise Exception("Arc length is 0.") @classmethod - def construct_from_points(cls, point_start, point_end, point_center, + def construct_with_points(cls, point_start, point_end, point_center, arc_winding_ccw=True): """ - Construct an arc segment from three points (start, end, center). + Construct an arc segment with three points (start, end, center). :param point_start: Starting point of the segment :param point_end: End point of the segment @@ -350,7 +350,7 @@ def construct_with_radius(cls, point_start, point_end, radius, vec_start_center = 0.5 * vec_start_end + vec_normal * normal_scaling point_center = point_start + vec_start_center - return cls.construct_from_points(point_start, point_end, point_center, + return cls.construct_with_points(point_start, point_end, point_center, arc_winding_ccw) @classmethod diff --git a/tests/test_geometry.py b/tests/test_geometry.py index db9dd89..942bd92 100644 --- a/tests/test_geometry.py +++ b/tests/test_geometry.py @@ -146,7 +146,7 @@ def test_line_segment_construction(): geo.LineSegment([[[3, 5], [3, 4]]]) # factories ------------------------------------------- - segment = geo.LineSegment.construct_from_points([3, 3], [4, 5]) + segment = geo.LineSegment.construct_with_points([3, 3], [4, 5]) assert math.isclose(segment.length, np.sqrt(5)) @@ -181,7 +181,7 @@ def test_line_segment_rasterization(): def test_line_segment_transformations(): # translation ----------------------------------------- - segment = geo.LineSegment.construct_from_points([3, 3], [4, 5]) + segment = geo.LineSegment.construct_with_points([3, 3], [4, 5]) segment.translate([-1, 4]) helpers.check_vectors_identical(segment.point_start, np.array([2, 7])) @@ -193,7 +193,7 @@ def test_line_segment_transformations(): c = np.cos(np.pi / 4.) rotation_matrix = [[c, -s], [s, c]] - segment = geo.LineSegment.construct_from_points([2, 2], [3, 6]) + segment = geo.LineSegment.construct_with_points([2, 2], [3, 6]) segment.apply_transformation(rotation_matrix) exp_start = [0, np.sqrt(8)] @@ -207,7 +207,7 @@ def test_line_segment_transformations(): v = np.array([-1, 1], dtype=float) reflection_matrix = np.identity(2) - 2 / np.dot(v, v) * np.outer(v, v) - segment = geo.LineSegment.construct_from_points([-1, 3], [6, 1]) + segment = geo.LineSegment.construct_with_points([-1, 3], [6, 1]) segment.apply_transformation(reflection_matrix) helpers.check_vectors_identical(segment.point_start, [3, -1]) @@ -217,7 +217,7 @@ def test_line_segment_transformations(): # scaling --------------------------------------------- scale_matrix = [[4, 0], [0, 0.5]] - segment = geo.LineSegment.construct_from_points([-2, 2], [1, 4]) + segment = geo.LineSegment.construct_with_points([-2, 2], [1, 4]) segment.apply_transformation(scale_matrix) helpers.check_vectors_identical(segment.point_start, [-8, 1]) @@ -234,8 +234,8 @@ def test_line_segment_transformations(): def test_line_segment_interpolation(): - segment_a = geo.LineSegment.construct_from_points([1, 3], [7, -3]) - segment_b = geo.LineSegment.construct_from_points([5, -5], [-1, 13]) + segment_a = geo.LineSegment.construct_with_points([1, 3], [7, -3]) + segment_b = geo.LineSegment.construct_with_points([5, -5], [-1, 13]) for i in range(5): weight = i / 4 @@ -262,7 +262,7 @@ def test_line_segment_interpolation(): # exceptions ------------------------------------------ # wrong types - arc_segment = geo.ArcSegment.construct_from_points([0, 0], [1, 1], [1, 0]) + arc_segment = geo.ArcSegment.construct_with_points([0, 0], [1, 1], [1, 0]) with pytest.raises(TypeError): geo.LineSegment.linear_interpolation(segment_a, arc_segment, weight) with pytest.raises(TypeError): @@ -293,7 +293,7 @@ def arc_segment_test(point_center, point_start, point_end, raster_width, radius_arc = np.linalg.norm(point_start - point_center) - arc_segment = geo.ArcSegment.construct_from_points(point_start, + arc_segment = geo.ArcSegment.construct_with_points(point_start, point_end, point_center, arc_winding_ccw) @@ -377,10 +377,10 @@ def test_arc_segment_factories(): arc_length_small = np.pi * 1.5 arc_length_large = np.pi * 4.5 - segment_cw = geo.ArcSegment.construct_from_points(point_start, point_end, + segment_cw = geo.ArcSegment.construct_with_points(point_start, point_end, point_center_right, False) - segment_ccw = geo.ArcSegment.construct_from_points(point_start, point_end, + segment_ccw = geo.ArcSegment.construct_with_points(point_start, point_end, point_center_right, True) @@ -488,9 +488,9 @@ def not_above_center(p, c): def test_arc_segment_transformations(): # translation ----------------------------------------- - segment_cw = geo.ArcSegment.construct_from_points([3, 3], [5, 5], [5, 3], + segment_cw = geo.ArcSegment.construct_with_points([3, 3], [5, 5], [5, 3], False) - segment_ccw = geo.ArcSegment.construct_from_points([3, 3], [5, 5], [5, 3], + segment_ccw = geo.ArcSegment.construct_with_points([3, 3], [5, 5], [5, 3], True) segment_cw.translate([-1, 4]) segment_ccw.translate([-1, 4]) @@ -514,9 +514,9 @@ def test_arc_segment_transformations(): c = np.cos(np.pi / 4.) rotation_matrix = [[c, -s], [s, c]] - segment_cw = geo.ArcSegment.construct_from_points([3, 3], [5, 5], [5, 3], + segment_cw = geo.ArcSegment.construct_with_points([3, 3], [5, 5], [5, 3], False) - segment_ccw = geo.ArcSegment.construct_from_points([3, 3], [5, 5], [5, 3], + segment_ccw = geo.ArcSegment.construct_with_points([3, 3], [5, 5], [5, 3], True) segment_cw.apply_transformation(rotation_matrix) segment_ccw.apply_transformation(rotation_matrix) @@ -534,9 +534,9 @@ def test_arc_segment_transformations(): v = np.array([-1, 1], dtype=float) reflection_matrix = np.identity(2) - 2 / np.dot(v, v) * np.outer(v, v) - segment_cw = geo.ArcSegment.construct_from_points([3, 2], [5, 4], [5, 2], + segment_cw = geo.ArcSegment.construct_with_points([3, 2], [5, 4], [5, 2], False) - segment_ccw = geo.ArcSegment.construct_from_points([3, 2], [5, 4], [5, 2], + segment_ccw = geo.ArcSegment.construct_with_points([3, 2], [5, 4], [5, 2], True) segment_cw.apply_transformation(reflection_matrix) segment_ccw.apply_transformation(reflection_matrix) @@ -555,9 +555,9 @@ def test_arc_segment_transformations(): # scaling both coordinates equally -------------------- scaling_matrix = [[4, 0], [0, 4]] - segment_cw = geo.ArcSegment.construct_from_points([3, 2], [5, 4], [5, 2], + segment_cw = geo.ArcSegment.construct_with_points([3, 2], [5, 4], [5, 2], False) - segment_ccw = geo.ArcSegment.construct_from_points([3, 2], [5, 4], [5, 2], + segment_ccw = geo.ArcSegment.construct_with_points([3, 2], [5, 4], [5, 2], True) segment_cw.apply_transformation(scaling_matrix) segment_ccw.apply_transformation(scaling_matrix) @@ -579,9 +579,9 @@ def test_arc_segment_transformations(): # non-uniform scaling which results in a valid arc ---- scaling_matrix = [[0.25, 0], [0, 2]] - segment_cw = geo.ArcSegment.construct_from_points([8, 4], [32, 4], [20, 2], + segment_cw = geo.ArcSegment.construct_with_points([8, 4], [32, 4], [20, 2], False) - segment_ccw = geo.ArcSegment.construct_from_points([8, 4], [32, 4], + segment_ccw = geo.ArcSegment.construct_with_points([8, 4], [32, 4], [20, 2], True) segment_cw.apply_transformation(scaling_matrix) segment_ccw.apply_transformation(scaling_matrix) @@ -605,13 +605,13 @@ def test_arc_segment_transformations(): # exceptions ------------------------------------------ # transformation distorts arc - segment = geo.ArcSegment.construct_from_points([3, 2], [5, 4], [5, 2], + segment = geo.ArcSegment.construct_with_points([3, 2], [5, 4], [5, 2], False) with pytest.raises(Exception): segment.apply_transformation(scaling_matrix) # transformation results in length = 0 - segment = geo.ArcSegment.construct_from_points([3, 2], [5, 4], [5, 2], + segment = geo.ArcSegment.construct_with_points([3, 2], [5, 4], [5, 2], False) zero_matrix = np.zeros((2, 2)) with pytest.raises(Exception): @@ -619,8 +619,8 @@ def test_arc_segment_transformations(): def test_arc_segment_interpolation(): - segment_a = geo.ArcSegment.construct_from_points([0, 0], [1, 1], [1, 0]) - segment_b = geo.ArcSegment.construct_from_points([0, 0], [2, 2], [0, 2]) + segment_a = geo.ArcSegment.construct_with_points([0, 0], [1, 1], [1, 0]) + segment_b = geo.ArcSegment.construct_with_points([0, 0], [2, 2], [0, 2]) # not implemented yet with pytest.raises(Exception): @@ -630,8 +630,8 @@ def test_arc_segment_interpolation(): # test Shape ------------------------------------------------------------------ def test_shape_construction(): - line_segment = geo.LineSegment.construct_from_points([1, 1], [1, 2]) - arc_segment = geo.ArcSegment.construct_from_points([0, 0], [1, 1], [0, 1]) + line_segment = geo.LineSegment.construct_with_points([1, 1], [1, 2]) + arc_segment = geo.ArcSegment.construct_with_points([0, 0], [1, 1], [0, 1]) # Empty construction shape = geo.Shape() @@ -656,9 +656,9 @@ def test_shape_construction(): def test_shape_segment_addition(): # Create shape and add segments - line_segment = geo.LineSegment.construct_from_points([1, 1], [0, 0]) - arc_segment = geo.ArcSegment.construct_from_points([0, 0], [1, 1], [0, 1]) - arc_segment2 = geo.ArcSegment.construct_from_points([1, 1], [0, 0], [0, 1]) + line_segment = geo.LineSegment.construct_with_points([1, 1], [0, 0]) + arc_segment = geo.ArcSegment.construct_with_points([0, 0], [1, 1], [0, 1]) + arc_segment2 = geo.ArcSegment.construct_with_points([1, 1], [0, 0], [0, 1]) shape = geo.Shape() shape.add_segments(line_segment) @@ -695,11 +695,11 @@ def test_shape_rasterization(): raster_width = 0.2 shape = geo.Shape( - geo.LineSegment.construct_from_points(points[0], points[1])) + geo.LineSegment.construct_with_points(points[0], points[1])) shape.add_segments( - geo.LineSegment.construct_from_points(points[1], points[2])) + geo.LineSegment.construct_with_points(points[1], points[2])) shape.add_segments( - geo.LineSegment.construct_from_points(points[2], points[3])) + geo.LineSegment.construct_with_points(points[2], points[3])) data = shape.rasterize(raster_width) @@ -715,8 +715,8 @@ def test_shape_rasterization(): def default_test_shape(): # create shape - arc_segment = geo.ArcSegment.construct_from_points([3, 4], [5, 0], [6, 3]) - line_segment = geo.LineSegment.construct_from_points([5, 0], [11, 3]) + arc_segment = geo.ArcSegment.construct_with_points([3, 4], [5, 0], [6, 3]) + line_segment = geo.LineSegment.construct_with_points([5, 0], [11, 3]) return geo.Shape([arc_segment, line_segment]) @@ -885,12 +885,12 @@ def interpolation_nearest(segment_a, segment_b, weight): def test_shape_interpolation_general(): - segment_a0 = geo.LineSegment.construct_from_points([-1, -1], [1, 1]) - segment_a1 = geo.LineSegment.construct_from_points([1, 1], [3, -1]) + segment_a0 = geo.LineSegment.construct_with_points([-1, -1], [1, 1]) + segment_a1 = geo.LineSegment.construct_with_points([1, 1], [3, -1]) shape_a = geo.Shape([segment_a0, segment_a1]) - segment_b0 = geo.LineSegment.construct_from_points([-1, 4], [1, 1]) - segment_b1 = geo.LineSegment.construct_from_points([1, 1], [3, 4]) + segment_b0 = geo.LineSegment.construct_with_points([-1, 4], [1, 1]) + segment_b1 = geo.LineSegment.construct_with_points([1, 1], [3, 4]) shape_b = geo.Shape([segment_b0, segment_b1]) interpolations = [geo.LineSegment.linear_interpolation, @@ -901,10 +901,10 @@ def test_shape_interpolation_general(): interpolations) assert shape_c.num_segments == 2 - exp_segment_c0 = geo.LineSegment.construct_from_points( + exp_segment_c0 = geo.LineSegment.construct_with_points( [-1, -1 + 5 * weight], [1, 1]) check_segments_identical(shape_c.segments[0], exp_segment_c0) - + if weight > 0.5: check_segments_identical(shape_c.segments[1], segment_b1) else: @@ -912,12 +912,12 @@ def test_shape_interpolation_general(): def test_shape_linear_interpolation(): - segment_a0 = geo.LineSegment.construct_from_points([0, 0], [1, 1]) - segment_a1 = geo.LineSegment.construct_from_points([1, 1], [2, 0]) + segment_a0 = geo.LineSegment.construct_with_points([0, 0], [1, 1]) + segment_a1 = geo.LineSegment.construct_with_points([1, 1], [2, 0]) shape_a = geo.Shape([segment_a0, segment_a1]) - segment_b0 = geo.LineSegment.construct_from_points([1, 1], [2, -1]) - segment_b1 = geo.LineSegment.construct_from_points([2, -1], [3, 5]) + segment_b0 = geo.LineSegment.construct_with_points([1, 1], [2, -1]) + segment_b1 = geo.LineSegment.construct_with_points([2, -1], [3, 5]) shape_b = geo.Shape([segment_b0, segment_b1]) for i in range(5): @@ -960,6 +960,6 @@ def test_shape_linear_interpolation(): # exceptions ------------------------------------------ - shape_a.add_segments(geo.LineSegment.construct_from_points([2, 0], [2, 2])) + shape_a.add_segments(geo.LineSegment.construct_with_points([2, 0], [2, 2])) with pytest.raises(Exception): geo.Shape.linear_interpolation(shape_a, shape_b, 0.25) diff --git a/tests/test_point_cloud_generator.py b/tests/test_point_cloud_generator.py index bab3fb3..be5fcf5 100644 --- a/tests/test_point_cloud_generator.py +++ b/tests/test_point_cloud_generator.py @@ -52,15 +52,15 @@ def get_default_profiles(): a_0 = [0, 0] a_1 = [8, 16] a_2 = [16, 0] - shape_a01 = geo.Shape(geo.LineSegment.construct_from_points(a_0, a_1)) - shape_a12 = geo.Shape(geo.LineSegment.construct_from_points(a_1, a_2)) + shape_a01 = geo.Shape(geo.LineSegment.construct_with_points(a_0, a_1)) + shape_a12 = geo.Shape(geo.LineSegment.construct_with_points(a_1, a_2)) profile_a = pcg.Profile([shape_a01, shape_a12]) b_0 = [-4, 8] b_1 = [0, 8] b_2 = [16, -16] - shape_b01 = geo.Shape(geo.LineSegment.construct_from_points(b_0, b_1)) - shape_b12 = geo.Shape(geo.LineSegment.construct_from_points(b_1, b_2)) + shape_b01 = geo.Shape(geo.LineSegment.construct_with_points(b_0, b_1)) + shape_b12 = geo.Shape(geo.LineSegment.construct_with_points(b_1, b_2)) profile_b = pcg.Profile([shape_b01, shape_b12]) return [profile_a, profile_b] @@ -68,9 +68,9 @@ def get_default_profiles(): # Test profile class ---------------------------------------------------------- def test_profile_construction_and_shape_addition(): - segment0 = geo.LineSegment.construct_from_points([0, 0], [1, 0]) - segment1 = geo.LineSegment.construct_from_points([1, 0], [2, -1]) - segment2 = geo.LineSegment.construct_from_points([2, -1], [0, -1]) + segment0 = geo.LineSegment.construct_with_points([0, 0], [1, 0]) + segment1 = geo.LineSegment.construct_with_points([1, 0], [2, -1]) + segment2 = geo.LineSegment.construct_with_points([2, -1], [0, -1]) shape = geo.Shape([segment0, segment1, segment2]) @@ -129,10 +129,10 @@ def test_profile_construction_and_shape_addition(): def test_profile_rasterization(): raster_width = 0.1 shape0 = geo.Shape( - geo.LineSegment.construct_from_points([-1, 0], [-raster_width, 0])) - shape1 = geo.Shape(geo.LineSegment.construct_from_points([0, 0], [1, 0])) + geo.LineSegment.construct_with_points([-1, 0], [-raster_width, 0])) + shape1 = geo.Shape(geo.LineSegment.construct_with_points([0, 0], [1, 0])) shape2 = geo.Shape( - geo.LineSegment.construct_from_points([1 + raster_width, 0], [2, 0])) + geo.LineSegment.construct_with_points([1 + raster_width, 0], [2, 0])) profile = pcg.Profile([shape0, shape1]) profile.add_shapes(shape2) @@ -430,15 +430,15 @@ def test_linear_profile_interpolation_sbs(): a_0 = [0, 0] a_1 = [8, 16] a_2 = [16, 0] - shape_a01 = geo.Shape(geo.LineSegment.construct_from_points(a_0, a_1)) - shape_a12 = geo.Shape(geo.LineSegment.construct_from_points(a_1, a_2)) + shape_a01 = geo.Shape(geo.LineSegment.construct_with_points(a_0, a_1)) + shape_a12 = geo.Shape(geo.LineSegment.construct_with_points(a_1, a_2)) profile_a = pcg.Profile([shape_a01, shape_a12]) b_0 = [-4, 8] b_1 = [0, 8] b_2 = [16, -16] - shape_b01 = geo.Shape(geo.LineSegment.construct_from_points(b_0, b_1)) - shape_b12 = geo.Shape(geo.LineSegment.construct_from_points(b_1, b_2)) + shape_b01 = geo.Shape(geo.LineSegment.construct_with_points(b_0, b_1)) + shape_b12 = geo.Shape(geo.LineSegment.construct_with_points(b_1, b_2)) profile_b = pcg.Profile([shape_b01, shape_b12]) [profile_a, profile_b] = get_default_profiles() @@ -476,8 +476,8 @@ def test_linear_profile_interpolation_sbs(): 0.5) # number of segments differ - shape_b012 = geo.Shape([geo.LineSegment.construct_from_points(b_0, b_1), - geo.LineSegment.construct_from_points(b_1, b_2)]) + shape_b012 = geo.Shape([geo.LineSegment.construct_with_points(b_0, b_1), + geo.LineSegment.construct_with_points(b_1, b_2)]) profile_b2 = pcg.Profile([shape_b01, shape_b012]) with pytest.raises(Exception): @@ -647,10 +647,10 @@ def test_geometry_rasterization_trace(): a3 = [1, 1] a4 = [1, 0] - shape_a012 = geo.Shape([geo.LineSegment.construct_from_points(a0, a1), - geo.LineSegment.construct_from_points(a1, a2)]) - shape_a234 = geo.Shape([geo.LineSegment.construct_from_points(a2, a3), - geo.LineSegment.construct_from_points(a3, a4)]) + shape_a012 = geo.Shape([geo.LineSegment.construct_with_points(a0, a1), + geo.LineSegment.construct_with_points(a1, a2)]) + shape_a234 = geo.Shape([geo.LineSegment.construct_with_points(a2, a3), + geo.LineSegment.construct_with_points(a3, a4)]) profile_a = pcg.Profile([shape_a012, shape_a234]) @@ -723,10 +723,10 @@ def test_geometry_rasterization_profile_interpolation(): a3 = [1, 1] a4 = [1, 0] - shape_a012 = geo.Shape([geo.LineSegment.construct_from_points(a0, a1), - geo.LineSegment.construct_from_points(a1, a2)]) - shape_a234 = geo.Shape([geo.LineSegment.construct_from_points(a2, a3), - geo.LineSegment.construct_from_points(a3, a4)]) + shape_a012 = geo.Shape([geo.LineSegment.construct_with_points(a0, a1), + geo.LineSegment.construct_with_points(a1, a2)]) + shape_a234 = geo.Shape([geo.LineSegment.construct_with_points(a2, a3), + geo.LineSegment.construct_with_points(a3, a4)]) shape_b012 = copy.deepcopy(shape_a012) shape_b234 = copy.deepcopy(shape_a234) From b659d999a23cfde8746575ba290986d55e334688 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Mon, 3 Feb 2020 14:59:41 +0100 Subject: [PATCH 133/177] Fix deepsource issue --- tests/test_geometry.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_geometry.py b/tests/test_geometry.py index 942bd92..9b90786 100644 --- a/tests/test_geometry.py +++ b/tests/test_geometry.py @@ -880,8 +880,7 @@ def test_shape_reflect(): def interpolation_nearest(segment_a, segment_b, weight): if weight > 0.5: return segment_b - else: - return segment_a + return segment_a def test_shape_interpolation_general(): From dd694eba942b162880039fb3965accdb953c2edc Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Mon, 3 Feb 2020 17:37:50 +0100 Subject: [PATCH 134/177] Use local yz-plane of trace as profile plane --- mypackage/point_cloud_generator.py | 6 +-- tests/test_point_cloud_generator.py | 62 +++++++++++++++-------------- 2 files changed, 35 insertions(+), 33 deletions(-) diff --git a/mypackage/point_cloud_generator.py b/mypackage/point_cloud_generator.py index dd42153..88dfa50 100644 --- a/mypackage/point_cloud_generator.py +++ b/mypackage/point_cloud_generator.py @@ -102,7 +102,7 @@ def local_coordinate_system(self, relative_position): """ relative_position = np.clip(relative_position, 0, 1) - origin = np.array([0, 1, 0]) * relative_position * self._length + origin = np.array([1, 0, 0]) * relative_position * self._length return tf.CoordinateSystem(origin=origin) @@ -188,7 +188,7 @@ def local_coordinate_system(self, relative_position): basis = tf.rotation_matrix_z( self._angle * relative_position * self._sign_winding) - translation = np.array([1, 0, 0]) * self._radius * self._sign_winding + translation = np.array([0, -1, 0]) * self._radius * self._sign_winding origin = np.matmul(basis, translation) - translation return tf.CoordinateSystem(basis, origin) @@ -558,7 +558,7 @@ def _profile_raster_data_3d(profile, raster_width): :return: Rasterized profile in 3d """ profile_data = profile.rasterize(raster_width) - return np.insert(profile_data, 1, 0, axis=0) + return np.insert(profile_data, 0, 0, axis=0) def _rasterize_constant_profile(self, profile_raster_width, trace_raster_width): diff --git a/tests/test_point_cloud_generator.py b/tests/test_point_cloud_generator.py index be5fcf5..e6ff69f 100644 --- a/tests/test_point_cloud_generator.py +++ b/tests/test_point_cloud_generator.py @@ -198,7 +198,7 @@ def check_trace_segment_orientation(segment): trace_direction_numerical = tf.normalize(lcs.origin - lcs_d.origin) # Check that the y-axis is always aligned with the trace's direction - helpers.check_vectors_identical(lcs.basis[:, 1], + helpers.check_vectors_identical(lcs.basis[:, 0], trace_direction_numerical, 1E-6) @@ -254,15 +254,15 @@ def test_radial_horizontal_trace_segment(): # check positions for weight in np.arange(0.1, 1, 0.1): current_angle = angle * weight - x_exp = (1 - np.cos(current_angle)) * radius - y_exp = np.sin(current_angle) * radius + x_exp = np.sin(current_angle) * radius + y_exp = (1 - np.cos(current_angle)) * radius lcs_cw = segment_cw.local_coordinate_system(weight) lcs_ccw = segment_ccw.local_coordinate_system(weight) assert math.isclose(lcs_cw.origin[0], x_exp) - assert math.isclose(lcs_cw.origin[1], y_exp) - assert math.isclose(lcs_ccw.origin[0], -x_exp) + assert math.isclose(lcs_cw.origin[1], -y_exp) + assert math.isclose(lcs_ccw.origin[0], x_exp) assert math.isclose(lcs_ccw.origin[1], y_exp) # invalid inputs @@ -371,7 +371,7 @@ def test_trace_local_coordinate_system(): position_on_segment = linear_segment.length * weight position = radial_segment.length + position_on_segment - expected_origin = np.array([-2, -position_on_segment, 0]) + expected_origin = np.array([-position_on_segment, 2, 0]) cs_trace = trace.local_coordinate_system(position) helpers.check_matrices_identical(cs_trace.basis, expected_basis) @@ -405,7 +405,7 @@ def test_trace_local_coordinate_system(): position_on_segment = linear_segment.length * weight position = radial_segment.length + position_on_segment - expected_origin = np.array([-2, 0, -position_on_segment]) + origin + expected_origin = np.array([-position_on_segment, 0, 2]) + origin cs_trace = trace.local_coordinate_system(position) helpers.check_matrices_identical(cs_trace.basis, expected_basis) @@ -641,11 +641,11 @@ def test_geometry_construction(): def test_geometry_rasterization_trace(): - a0 = [-1, 0] - a1 = [-1, 1] + a0 = [1, 0] + a1 = [1, 1] a2 = [0, 1] - a3 = [1, 1] - a4 = [1, 0] + a3 = [-1, 1] + a4 = [-1, 0] shape_a012 = geo.Shape([geo.LineSegment.construct_with_points(a0, a1), geo.LineSegment.construct_with_points(a1, a2)]) @@ -654,7 +654,7 @@ def test_geometry_rasterization_trace(): profile_a = pcg.Profile([shape_a012, shape_a234]) - radial_segment = pcg.RadialHorizontalTraceSegment(1, np.pi / 2) + radial_segment = pcg.RadialHorizontalTraceSegment(1, np.pi / 2, False) linear_segment = pcg.LinearHorizontalTraceSegment(1) trace = pcg.Trace([linear_segment, radial_segment]) @@ -674,20 +674,19 @@ def test_geometry_rasterization_trace(): for i in range(num_raster_profiles): idx_0 = i * 6 - if data[1, idx_0 + 2] <= 1: - y = data[1, idx_0] - assert math.isclose(y, eff_raster_width * i, abs_tol=1E-6) - + if data[0, idx_0 + 2] <= 1: + x = data[0, idx_0] + assert math.isclose(x, eff_raster_width * i, abs_tol=1E-6) for j in range(6): - assert math.isclose(data[0, idx_0 + j], profile_points[0, j]) + assert math.isclose(data[1, idx_0 + j], profile_points[0, j]) assert math.isclose(data[2, idx_0 + j], profile_points[1, j]) - assert math.isclose(data[1, idx_0 + j], data[1, idx_0]) + assert math.isclose(data[0, idx_0 + j], data[0, idx_0]) else: - assert math.isclose(data[0, idx_0], a0[0]) - assert math.isclose(data[1, idx_0], 1) + assert math.isclose(data[0, idx_0], 1) + assert math.isclose(data[1, idx_0], a0[0]) assert math.isclose(data[2, idx_0], a0[1]) - assert math.isclose(data[0, idx_0 + 1], a1[0]) - assert math.isclose(data[1, idx_0 + 1], 1) + assert math.isclose(data[0, idx_0 + 1], 1) + assert math.isclose(data[1, idx_0 + 1], a1[0]) assert math.isclose(data[2, idx_0 + 1], a1[1]) # z-values are constant @@ -714,14 +713,17 @@ def test_geometry_rasterization_trace(): point_distance) +test_geometry_rasterization_trace() + + def test_geometry_rasterization_profile_interpolation(): interpol = pcg.LinearProfileInterpolationSBS - a0 = [-1, 0] - a1 = [-1, 1] + a0 = [1, 0] + a1 = [1, 1] a2 = [0, 1] - a3 = [1, 1] - a4 = [1, 0] + a3 = [-1, 1] + a4 = [-1, 0] shape_a012 = geo.Shape([geo.LineSegment.construct_with_points(a0, a1), geo.LineSegment.construct_with_points(a1, a2)]) @@ -760,8 +762,8 @@ def test_geometry_rasterization_profile_interpolation(): for i in range(11): idx_0 = i * 6 for j in range(6): - exp_point = np.array([profile_points[0, j] * (1 + i * 0.1), - i * 0.1, + exp_point = np.array([i * 0.1, + profile_points[0, j] * (1 + i * 0.1), profile_points[1, j] * (1 + i * 0.1)]) helpers.check_vectors_identical(data[:, idx_0 + j], exp_point) @@ -769,7 +771,7 @@ def test_geometry_rasterization_profile_interpolation(): for i in range(20): idx_0 = (30 - i) * 6 for j in range(6): - exp_point = np.array([profile_points[0, j] * (1 + i * 0.05), - 3 - i * 0.1, + exp_point = np.array([3 - i * 0.1, + profile_points[0, j] * (1 + i * 0.05), profile_points[1, j] * (1 + i * 0.05)]) helpers.check_vectors_identical(data[:, idx_0 + j], exp_point) From f7b75d707a264d5eed041e7a13f4bd67d18ac7f9 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Tue, 4 Feb 2020 11:59:19 +0100 Subject: [PATCH 135/177] Add trace rasterization --- mypackage/point_cloud_generator.py | 41 ++++++++++++++++++++--- tests/test_point_cloud_generator.py | 51 ++++++++++++++++++++++++++++- 2 files changed, 86 insertions(+), 6 deletions(-) diff --git a/mypackage/point_cloud_generator.py b/mypackage/point_cloud_generator.py index 88dfa50..125ee85 100644 --- a/mypackage/point_cloud_generator.py +++ b/mypackage/point_cloud_generator.py @@ -231,15 +231,14 @@ def _create_lookups(self, coordinate_system_start): segments = self._segments - # Fill coordinate system lookup - for i in range(len(segments) - 1): + total_length = 0 + for i, segment in enumerate(segments): + # Fill coordinate system lookup lcs_segment_end = segments[i].local_coordinate_system(1) csys = self._coordinate_system_lookup[i] + lcs_segment_end self._coordinate_system_lookup += [csys] - # Fill length lookups - total_length = 0 - for _, segment in enumerate(segments): + # Fill length lookups segment_length = segment.length total_length += segment_length self._segment_length_lookup += [segment_length] @@ -312,6 +311,38 @@ def local_coordinate_system(self, position): return segment_start_cs + local_segment_cs + def rasterize(self, raster_width): + """ + Rasterize the trace. + + :return: Raster data + """ + raster_width = np.clip(raster_width, 0, self.length) + num_raster_segments = int(np.round(self.length / raster_width)) + raster_width_eff = self.length / num_raster_segments + + idx = 0 + raster_data = np.empty((3, 0)) + for i in range(num_raster_segments): + location = i * raster_width_eff + while not location <= self._total_length_lookup[idx + 1]: + idx += 1 + + segment_location = location - self._total_length_lookup[idx] + weight = segment_location / self._segment_length_lookup[idx] + + local_segment_cs = self.segments[idx].local_coordinate_system( + weight) + segment_start_cs = self._coordinate_system_lookup[idx] + + local_cs = segment_start_cs + local_segment_cs + + data_point = local_cs.origin[:, np.newaxis] + raster_data = np.hstack([raster_data, data_point]) + + last_point = self._coordinate_system_lookup[-1].origin[:, np.newaxis] + return np.hstack([raster_data, last_point]) + # Linear profile interpolation class ------------------------------------------ diff --git a/tests/test_point_cloud_generator.py b/tests/test_point_cloud_generator.py index e6ff69f..625d22d 100644 --- a/tests/test_point_cloud_generator.py +++ b/tests/test_point_cloud_generator.py @@ -333,7 +333,7 @@ def __init__(self): @staticmethod def local_coordinate_system(*args): - return tf.CoordinateSystem + return tf.CoordinateSystem() custom_segment = CustomSegment() custom_segment.length = 3 @@ -412,6 +412,55 @@ def test_trace_local_coordinate_system(): helpers.check_vectors_identical(cs_trace.origin, expected_origin) +def test_trace_rasterization(): + radial_segment = pcg.RadialHorizontalTraceSegment(1, np.pi) + linear_segment = pcg.LinearHorizontalTraceSegment(1) + + # check with default coordinate system ---------------- + trace = pcg.Trace([linear_segment, radial_segment]) + data = trace.rasterize(0.1) + + raster_width_eff = trace.length / (data.shape[1] - 1) + for i in range(data.shape[1]): + trace_location = i * raster_width_eff + if trace_location <= 1: + helpers.check_vectors_identical([trace_location, 0, 0], data[:, i]) + else: + arc_location = trace_location - 1 + angle = arc_location # radius 1! + x = np.sin(angle) + 1 # radius 1! + y = 1 - np.cos(angle) + helpers.check_vectors_identical([x, y, 0], data[:, i]) + + # check with arbitrary coordinate system -------------- + basis = tf.rotation_matrix_y(np.pi / 2) + origin = np.array([-3, 2.5, 5]) + cs_base = tf.CoordinateSystem(basis, origin) + + trace = pcg.Trace([linear_segment, radial_segment], cs_base) + data = trace.rasterize(0.1) + + raster_width_eff = trace.length / (data.shape[1] - 1) + + for i in range(data.shape[1]): + trace_location = i * raster_width_eff + if trace_location <= 1: + x = origin[0] + y = origin[1] + z = origin[2] - trace_location + else: + arc_location = trace_location - 1 + angle = arc_location # radius 1! + x = origin[0] + y = origin[1] + 1 - np.cos(angle) + z = origin[2] - 1 - np.sin(angle) + + helpers.check_vectors_identical([x, y, z], data[:, i]) + + +test_trace_rasterization() + + # Profile interpolation classes ----------------------------------------------- From a2c0f2f61499462343fbefebfab7ee29ea15a642 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Tue, 4 Feb 2020 12:17:25 +0100 Subject: [PATCH 136/177] Apply some minor adjustments --- mypackage/geometry.py | 4 ++-- mypackage/point_cloud_generator.py | 8 ++++---- mypackage/transformations.py | 5 +++-- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/mypackage/geometry.py b/mypackage/geometry.py index 25a860a..77b74b0 100644 --- a/mypackage/geometry.py +++ b/mypackage/geometry.py @@ -72,7 +72,7 @@ def __init__(self, points): :param points: 2x2 matrix of points. The first column is the starting point and the second column the end point. """ - points = np.array(points, float) + points = ut.to_float_array(points) if not len(points.shape) == 2: raise ValueError("'points' must be a 2d array/matrix.") if not (points.shape[0] == 2 and points.shape[1] == 2): @@ -227,7 +227,7 @@ def __init__(self, points, arc_winding_ccw=True): :param: arc_winding_ccw: Specifies if the arcs winding order is counter-clockwise """ - points = np.array(points, float) + points = ut.to_float_array(points) if not len(points.shape) == 2: raise ValueError("'points' must be a 2d array/matrix.") if not (points.shape[0] == 2 and points.shape[1] == 3): diff --git a/mypackage/point_cloud_generator.py b/mypackage/point_cloud_generator.py index 125ee85..a35a279 100644 --- a/mypackage/point_cloud_generator.py +++ b/mypackage/point_cloud_generator.py @@ -82,7 +82,7 @@ def __init__(self, length): """ if length <= 0: raise ValueError("'length' must have a positive value.") - self._length = length + self._length = float(length) @property def length(self): @@ -122,9 +122,9 @@ def __init__(self, radius, angle, clockwise=False): raise ValueError("'radius' must have a positive value.") if angle <= 0: raise ValueError("'angle' must have a positive value.") - self._radius = radius - self._angle = angle - self._length = self._arc_length(radius, angle) + self._radius = float(radius) + self._angle = float(angle) + self._length = self._arc_length(self.radius, self.angle) if clockwise: self._sign_winding = -1 else: diff --git a/mypackage/transformations.py b/mypackage/transformations.py index 58cf245..3d4b9a1 100644 --- a/mypackage/transformations.py +++ b/mypackage/transformations.py @@ -1,5 +1,6 @@ """Contains methods and classes for coordinate transformations.""" +import mypackage._utility as ut import numpy as np import math from scipy.spatial.transform import Rotation as Rot @@ -164,7 +165,7 @@ def __init__(self, basis=np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]]), :param origin: Position of the origin :return: Cartesian coordinate system """ - basis = np.array(basis, dtype=float) + basis = ut.to_float_array(basis) basis[:, 0] = normalize(basis[:, 0]) basis[:, 1] = normalize(basis[:, 1]) basis[:, 2] = normalize(basis[:, 2]) @@ -176,7 +177,7 @@ def __init__(self, basis=np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]]), self._orientation = basis - self._location = np.array(origin) + self._location = ut.to_float_array(origin) def __add__(self, rhs_cs): """ From daf46873b72a1722c9e803b134068a1efc955087 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Tue, 4 Feb 2020 12:26:01 +0100 Subject: [PATCH 137/177] Move some functions to transformation package --- mypackage/geometry.py | 57 ++-------------------------------- mypackage/transformations.py | 50 ++++++++++++++++++++++++++++++ tests/test_geometry.py | 60 ------------------------------------ tests/test_trasformations.py | 60 ++++++++++++++++++++++++++++++++++++ 4 files changed, 112 insertions(+), 115 deletions(-) diff --git a/mypackage/geometry.py b/mypackage/geometry.py index 77b74b0..cffeee8 100644 --- a/mypackage/geometry.py +++ b/mypackage/geometry.py @@ -7,59 +7,6 @@ from scipy.spatial.transform import Rotation as Rot -# Helper functions ------------------------------------------------------------ - - -def vector_points_to_left_of_vector(vector, vector_reference): - """ - Determine if a vector points to the left of another vector. - - Returns 1 if the vector points to the left of the reference vector and - -1 if it points to the right. In case both vectors point into the same - or the opposite directions, this function returns 0. - - :param vector: Vector - :param vector_reference: Reference vector - :return: 1,-1 or 0 (see description) - """ - return int(np.sign(np.linalg.det([vector_reference, vector]))) - - -def point_left_of_line(point, line_start, line_end): - """ - Determine if a point lies left of a line. - - Returns 1 if the point is left of the line and -1 if it is to the right. - If the point is located on the line, this function returns 0. - - :param point: Point - :param line_start: Starting point of the line - :param line_end: End point of the line - :return: 1,-1 or 0 (see description) - """ - vec_line_start_end = line_end - line_start - vec_line_start_point = point - line_start - return vector_points_to_left_of_vector(vec_line_start_point, - vec_line_start_end) - - -def reflection_sign(matrix): - """ - Get a sign indicating if the transformation is a reflection. - - Returns -1 if the transformation contains a reflection and 1 if not. - - :param matrix: Transformation matrix - :return: 1 or -1 (see description) - """ - sign = int(np.sign(np.linalg.det(matrix))) - - if sign == 0: - raise Exception("Invalid transformation") - - return sign - - # LineSegment ----------------------------------------------------------------- class LineSegment: @@ -261,7 +208,7 @@ def _calculate_arc_angle(self): dot_unit = np.dot(unit_center_start, unit_center_end) angle_vecs = np.arccos(np.clip(dot_unit, -1, 1)) - sign_winding_points = vector_points_to_left_of_vector( + sign_winding_points = tf.vector_points_to_left_of_vector( unit_center_end, unit_center_start) if np.abs(sign_winding_points + self._sign_arc_winding) > 0: @@ -465,7 +412,7 @@ def apply_transformation(self, matrix): :return: --- """ self._points = np.matmul(matrix, self._points) - self._sign_arc_winding *= reflection_sign(matrix) + self._sign_arc_winding *= tf.reflection_sign(matrix) self._calculate_arc_parameters() def rasterize(self, raster_width, num_points_excluded_end=0): diff --git a/mypackage/transformations.py b/mypackage/transformations.py index 3d4b9a1..6560aa5 100644 --- a/mypackage/transformations.py +++ b/mypackage/transformations.py @@ -147,6 +147,56 @@ def change_of_basis_translation(ccs_from, ccs_to): return ccs_from.origin - ccs_to.origin +def point_left_of_line(point, line_start, line_end): + """ + Determine if a point lies left of a line. + + Returns 1 if the point is left of the line and -1 if it is to the right. + If the point is located on the line, this function returns 0. + + :param point: Point + :param line_start: Starting point of the line + :param line_end: End point of the line + :return: 1,-1 or 0 (see description) + """ + vec_line_start_end = line_end - line_start + vec_line_start_point = point - line_start + return vector_points_to_left_of_vector(vec_line_start_point, + vec_line_start_end) + + +def reflection_sign(matrix): + """ + Get a sign indicating if the transformation is a reflection. + + Returns -1 if the transformation contains a reflection and 1 if not. + + :param matrix: Transformation matrix + :return: 1 or -1 (see description) + """ + sign = int(np.sign(np.linalg.det(matrix))) + + if sign == 0: + raise Exception("Invalid transformation") + + return sign + + +def vector_points_to_left_of_vector(vector, vector_reference): + """ + Determine if a vector points to the left of another vector. + + Returns 1 if the vector points to the left of the reference vector and + -1 if it points to the right. In case both vectors point into the same + or the opposite directions, this function returns 0. + + :param vector: Vector + :param vector_reference: Reference vector + :return: 1,-1 or 0 (see description) + """ + return int(np.sign(np.linalg.det([vector_reference, vector]))) + + # cartesian coordinate system class ------------------------------------------- class CoordinateSystem: diff --git a/tests/test_geometry.py b/tests/test_geometry.py index 9b90786..9ecf9bb 100644 --- a/tests/test_geometry.py +++ b/tests/test_geometry.py @@ -19,66 +19,6 @@ def check_segments_identical(a, b): helpers.check_vectors_identical(a.point_center, b.point_center) -def test_vector_points_to_left_of_vector(): - assert geo.vector_points_to_left_of_vector([-0.1, 1], [0, 1]) > 0 - assert geo.vector_points_to_left_of_vector([-0.1, -1], [0, 1]) > 0 - assert geo.vector_points_to_left_of_vector([3, 5], [1, 0]) > 0 - assert geo.vector_points_to_left_of_vector([-3, 5], [1, 0]) > 0 - assert geo.vector_points_to_left_of_vector([0, -0.1], [-4, 2]) > 0 - assert geo.vector_points_to_left_of_vector([-1, -0.1], [-4, 2]) > 0 - - assert geo.vector_points_to_left_of_vector([0.1, 1], [0, 1]) < 0 - assert geo.vector_points_to_left_of_vector([0.1, -1], [0, 1]) < 0 - assert geo.vector_points_to_left_of_vector([3, -5], [1, 0]) < 0 - assert geo.vector_points_to_left_of_vector([-3, -5], [1, 0]) < 0 - assert geo.vector_points_to_left_of_vector([0, 0.1], [-4, 2]) < 0 - assert geo.vector_points_to_left_of_vector([1, -0.1], [-4, 2]) < 0 - - assert geo.vector_points_to_left_of_vector([4, 4], [2, 2]) == 0 - assert geo.vector_points_to_left_of_vector([-4, -4], [2, 2]) == 0 - - -def test_point_left_of_line(): - line_start = np.array([2, 3]) - line_end = np.array([5, 6]) - assert geo.point_left_of_line([-8, 10], line_start, line_end) > 0 - assert geo.point_left_of_line([3, 0], line_start, line_end) < 0 - assert geo.point_left_of_line(line_start, line_start, line_end) == 0 - - line_start = np.array([2, 3]) - line_end = np.array([1, -4]) - assert geo.point_left_of_line([3, 0], line_start, line_end) > 0 - assert geo.point_left_of_line([-8, 10], line_start, line_end) < 0 - assert geo.point_left_of_line(line_start, line_start, line_end) == 0 - - -def test_reflection_sign(): - assert geo.reflection_sign([[-1, 0], [0, 1]]) == -1 - assert geo.reflection_sign([[1, 0], [0, -1]]) == -1 - assert geo.reflection_sign([[0, 1], [1, 0]]) == -1 - assert geo.reflection_sign([[0, -1], [-1, 0]]) == -1 - assert geo.reflection_sign([[-4, 0], [0, 2]]) == -1 - assert geo.reflection_sign([[6, 0], [0, -4]]) == -1 - assert geo.reflection_sign([[0, 3], [8, 0]]) == -1 - assert geo.reflection_sign([[0, -3], [-2, 0]]) == -1 - - assert geo.reflection_sign([[1, 0], [0, 1]]) == 1 - assert geo.reflection_sign([[-1, 0], [0, -1]]) == 1 - assert geo.reflection_sign([[0, -1], [1, 0]]) == 1 - assert geo.reflection_sign([[0, 1], [-1, 0]]) == 1 - assert geo.reflection_sign([[5, 0], [0, 6]]) == 1 - assert geo.reflection_sign([[-3, 0], [0, -7]]) == 1 - assert geo.reflection_sign([[0, -8], [9, 0]]) == 1 - assert geo.reflection_sign([[0, 3], [-2, 0]]) == 1 - - with pytest.raises(Exception): - geo.reflection_sign([[0, 0], [0, 0]]) - with pytest.raises(Exception): - geo.reflection_sign([[1, 0], [0, 0]]) - with pytest.raises(Exception): - geo.reflection_sign([[2, 2], [1, 1]]) - - # helper for segment tests ---------------------------------------------------- def default_segment_rasterization_tests(segment, raster_width, point_start, diff --git a/tests/test_trasformations.py b/tests/test_trasformations.py index 78cf595..b6b0cb1 100644 --- a/tests/test_trasformations.py +++ b/tests/test_trasformations.py @@ -268,6 +268,66 @@ def test_change_of_basis_translation(): assert math.isclose(diff[j], expected_diff[j]) +def test_vector_points_to_left_of_vector(): + assert tf.vector_points_to_left_of_vector([-0.1, 1], [0, 1]) > 0 + assert tf.vector_points_to_left_of_vector([-0.1, -1], [0, 1]) > 0 + assert tf.vector_points_to_left_of_vector([3, 5], [1, 0]) > 0 + assert tf.vector_points_to_left_of_vector([-3, 5], [1, 0]) > 0 + assert tf.vector_points_to_left_of_vector([0, -0.1], [-4, 2]) > 0 + assert tf.vector_points_to_left_of_vector([-1, -0.1], [-4, 2]) > 0 + + assert tf.vector_points_to_left_of_vector([0.1, 1], [0, 1]) < 0 + assert tf.vector_points_to_left_of_vector([0.1, -1], [0, 1]) < 0 + assert tf.vector_points_to_left_of_vector([3, -5], [1, 0]) < 0 + assert tf.vector_points_to_left_of_vector([-3, -5], [1, 0]) < 0 + assert tf.vector_points_to_left_of_vector([0, 0.1], [-4, 2]) < 0 + assert tf.vector_points_to_left_of_vector([1, -0.1], [-4, 2]) < 0 + + assert tf.vector_points_to_left_of_vector([4, 4], [2, 2]) == 0 + assert tf.vector_points_to_left_of_vector([-4, -4], [2, 2]) == 0 + + +def test_point_left_of_line(): + line_start = np.array([2, 3]) + line_end = np.array([5, 6]) + assert tf.point_left_of_line([-8, 10], line_start, line_end) > 0 + assert tf.point_left_of_line([3, 0], line_start, line_end) < 0 + assert tf.point_left_of_line(line_start, line_start, line_end) == 0 + + line_start = np.array([2, 3]) + line_end = np.array([1, -4]) + assert tf.point_left_of_line([3, 0], line_start, line_end) > 0 + assert tf.point_left_of_line([-8, 10], line_start, line_end) < 0 + assert tf.point_left_of_line(line_start, line_start, line_end) == 0 + + +def test_reflection_sign(): + assert tf.reflection_sign([[-1, 0], [0, 1]]) == -1 + assert tf.reflection_sign([[1, 0], [0, -1]]) == -1 + assert tf.reflection_sign([[0, 1], [1, 0]]) == -1 + assert tf.reflection_sign([[0, -1], [-1, 0]]) == -1 + assert tf.reflection_sign([[-4, 0], [0, 2]]) == -1 + assert tf.reflection_sign([[6, 0], [0, -4]]) == -1 + assert tf.reflection_sign([[0, 3], [8, 0]]) == -1 + assert tf.reflection_sign([[0, -3], [-2, 0]]) == -1 + + assert tf.reflection_sign([[1, 0], [0, 1]]) == 1 + assert tf.reflection_sign([[-1, 0], [0, -1]]) == 1 + assert tf.reflection_sign([[0, -1], [1, 0]]) == 1 + assert tf.reflection_sign([[0, 1], [-1, 0]]) == 1 + assert tf.reflection_sign([[5, 0], [0, 6]]) == 1 + assert tf.reflection_sign([[-3, 0], [0, -7]]) == 1 + assert tf.reflection_sign([[0, -8], [9, 0]]) == 1 + assert tf.reflection_sign([[0, 3], [-2, 0]]) == 1 + + with pytest.raises(Exception): + tf.reflection_sign([[0, 0], [0, 0]]) + with pytest.raises(Exception): + tf.reflection_sign([[1, 0], [0, 0]]) + with pytest.raises(Exception): + tf.reflection_sign([[2, 2], [1, 1]]) + + # test cartesian coordinate system class -------------------------------------- def test_cartesian_coordinate_system_construction(): From 205052e1ef8161672ef26a5a73dedcb44da34f34 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Tue, 4 Feb 2020 17:45:07 +0100 Subject: [PATCH 138/177] Improve rasterization tests and methods --- mypackage/geometry.py | 15 ++++----- mypackage/point_cloud_generator.py | 3 ++ tests/_helpers.py | 6 ++++ tests/test_geometry.py | 48 +++++++++++++++++++++++------ tests/test_point_cloud_generator.py | 24 ++++++++++++++- 5 files changed, 78 insertions(+), 18 deletions(-) diff --git a/mypackage/geometry.py b/mypackage/geometry.py index cffeee8..3d36c92 100644 --- a/mypackage/geometry.py +++ b/mypackage/geometry.py @@ -132,9 +132,9 @@ def rasterize(self, raster_width, num_points_excluded_end=0): multiple segments. :return: Array of contour points """ - raster_width = np.clip(np.abs(raster_width), 0, self.length) if not raster_width > 0: - raise ValueError("'raster_width' is zero") + raise ValueError("'raster_width' must be > 0") + raster_width = np.clip(np.abs(raster_width), None, self.length) num_raster_segments = np.round(self.length / raster_width) @@ -434,9 +434,9 @@ def rasterize(self, raster_width, num_points_excluded_end=0): point_center = self.point_center vec_center_start = (point_start - point_center) - raster_width = np.clip(raster_width, 0, self.arc_length) if not raster_width > 0: - raise ValueError("'raster_width' is 0") + raise ValueError("'raster_width' must be > 0") + raster_width = np.clip(raster_width, None, self.arc_length) num_raster_segments = int(np.round(self._arc_length / raster_width)) delta_angle = self._arc_angle / num_raster_segments @@ -621,13 +621,14 @@ def rasterize(self, raster_width): :param raster_width: The desired distance between two raster points :return: Array of contour points (3d) """ - segments = self._segments + if not raster_width > 0: + raise ValueError("'raster_width' must be > 0") raster_data = np.empty([2, 0]) for i in range(self.num_segments): - segment_data = segments[i].rasterize(raster_width, 1) + segment_data = self.segments[i].rasterize(raster_width, 1) raster_data = np.hstack((raster_data, segment_data)) - last_point = segments[-1].point_end[:, np.newaxis] + last_point = self.segments[-1].point_end[:, np.newaxis] raster_data = np.hstack((raster_data, last_point)) return raster_data diff --git a/mypackage/point_cloud_generator.py b/mypackage/point_cloud_generator.py index a35a279..19f7085 100644 --- a/mypackage/point_cloud_generator.py +++ b/mypackage/point_cloud_generator.py @@ -317,6 +317,9 @@ def rasterize(self, raster_width): :return: Raster data """ + if not raster_width > 0: + raise ValueError("'raster_width' must be > 0") + raster_width = np.clip(raster_width, 0, self.length) num_raster_segments = int(np.round(self.length / raster_width)) raster_width_eff = self.length / num_raster_segments diff --git a/tests/_helpers.py b/tests/_helpers.py index ba2b67e..2bbc3a3 100644 --- a/tests/_helpers.py +++ b/tests/_helpers.py @@ -32,3 +32,9 @@ def rotated_coordinate_system(angle_x=np.pi / 3, angle_y=np.pi / 4, rotated_basis = np.matmul(r_tot, basis) return tf.CoordinateSystem(rotated_basis, np.array(origin)) + + +def are_all_points_unique(data, decimals=3): + unique = np.unique(np.round(data, decimals=decimals), axis=1) + return (unique.shape[0] == data.shape[0] and + unique.shape[1] == data.shape[1]) diff --git a/tests/test_geometry.py b/tests/test_geometry.py index 9ecf9bb..42ae299 100644 --- a/tests/test_geometry.py +++ b/tests/test_geometry.py @@ -44,6 +44,9 @@ def default_segment_rasterization_tests(segment, raster_width, point_start, raster_width_eff = np.linalg.norm(next_point - point) assert np.abs(raster_width_eff - raster_width) < 0.1 * raster_width + # check that there are no duplicate points + assert helpers.are_all_points_unique(data) + # check rasterization with excluded points data_m2 = segment.rasterize(raster_width, 2) @@ -62,9 +65,11 @@ def default_segment_rasterization_tests(segment, raster_width, point_start, helpers.check_vectors_identical(point_start, data_200[:, 0]) helpers.check_vectors_identical(point_end, data_200[:, 1]) - # check exceptions + # check exceptions when raster width <= 0 with pytest.raises(ValueError): segment.rasterize(0) + with pytest.raises(ValueError): + segment.rasterize(-3) # test LineSegment ------------------------------------------------------------ @@ -643,14 +648,37 @@ def test_shape_rasterization(): data = shape.rasterize(raster_width) - # Segment start and end points points must be included + # no duplications + assert helpers.are_all_points_unique(data) + + # check each data point + num_data_points = data.shape[1] + for i in range(num_data_points): + if i < 6: + helpers.check_vectors_identical([0, i * 0.2], data[:, i]) + elif i < 11: + helpers.check_vectors_identical([(i - 5) * 0.2, 1], data[:, i]) + else: + helpers.check_vectors_identical([1, 1 - (i - 10) * 0.2], + data[:, i]) + + # Test with too large raster width -------------------- + # The shape does not clip large values to the valid range itself. The + # added segments do the clipping. If a custom segment does not do that, + # there is currently no mechanism to correct it. + # However, this test somewhat ensures, that each segment is rasterized + # individually. + + data = shape.rasterize(10) + for point in points: assert utils.is_column_in_matrix(point, data) - # check effective raster width - for i in range(1, data.shape[1]): - raster_width_eff = np.linalg.norm(data[:, i] - data[:, i - 1]) - assert np.abs(raster_width_eff - raster_width) < 0.1 * raster_width + # exceptions ------------------------------------------ + with pytest.raises(Exception): + shape.rasterize(0) + with pytest.raises(Exception): + shape.rasterize(-3) def default_test_shape(): @@ -672,8 +700,8 @@ def check_point(point, point_ref, translation): # apply translation shape.translate(translation) - arc_segment = shape._segments[0] - arc_segment_ref = shape_ref._segments[0] + arc_segment = shape.segments[0] + arc_segment_ref = shape_ref.segments[0] assert (arc_segment.arc_winding_ccw == arc_segment_ref.arc_winding_ccw) @@ -684,8 +712,8 @@ def check_point(point, point_ref, translation): check_point(arc_segment.point_center, arc_segment_ref.point_center, translation) - line_segment = shape._segments[1] - line_segment_ref = shape_ref._segments[1] + line_segment = shape.segments[1] + line_segment_ref = shape_ref.segments[1] check_point(line_segment.point_start, line_segment_ref.point_start, translation) diff --git a/tests/test_point_cloud_generator.py b/tests/test_point_cloud_generator.py index 625d22d..4809170 100644 --- a/tests/test_point_cloud_generator.py +++ b/tests/test_point_cloud_generator.py @@ -140,6 +140,9 @@ def test_profile_rasterization(): # rasterize data = profile.rasterize(0.1) + # no duplications + assert helpers.are_all_points_unique(data) + # check raster data size expected_number_raster_points = int(round(3 / raster_width)) + 1 assert data.shape[1] == expected_number_raster_points @@ -150,6 +153,12 @@ def test_profile_rasterization(): assert data[0, i] - expected_raster_point_x < 1E-9 assert data[1, i] == 0 + # exceptions + with pytest.raises(Exception): + profile.rasterize(0) + with pytest.raises(Exception): + profile.rasterize(-3) + # Test trace segment classes -------------------------------------------------- @@ -420,6 +429,9 @@ def test_trace_rasterization(): trace = pcg.Trace([linear_segment, radial_segment]) data = trace.rasterize(0.1) + # no duplications + assert helpers.are_all_points_unique(data) + raster_width_eff = trace.length / (data.shape[1] - 1) for i in range(data.shape[1]): trace_location = i * raster_width_eff @@ -457,8 +469,18 @@ def test_trace_rasterization(): helpers.check_vectors_identical([x, y, z], data[:, i]) + # check if raster width is clipped to valid range ----- + data = trace.rasterize(1000) -test_trace_rasterization() + assert data.shape[1] == 2 + helpers.check_vectors_identical([-3, 2.5, 5], data[:, 0]) + helpers.check_vectors_identical([-3, 4.5, 4], data[:, 1]) + + # exceptions ------------------------------------------ + with pytest.raises(Exception): + trace.rasterize(0) + with pytest.raises(Exception): + trace.rasterize(-23.1) # Profile interpolation classes ----------------------------------------------- From 6a1847397c859e5a8a937e086c36b34514a5fd8f Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Wed, 5 Feb 2020 09:28:51 +0100 Subject: [PATCH 139/177] Add another testcase --- mypackage/point_cloud_generator.py | 5 ++++- tests/test_point_cloud_generator.py | 26 +++++++++++++++++++++++++- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/mypackage/point_cloud_generator.py b/mypackage/point_cloud_generator.py index 19f7085..a22d52a 100644 --- a/mypackage/point_cloud_generator.py +++ b/mypackage/point_cloud_generator.py @@ -560,7 +560,10 @@ def _rasterize_trace(self, raster_width): :param raster_width: Raster width :return: Raster data """ - raster_width = np.clip(raster_width, 0, self._trace.length) + if not raster_width > 0: + raise ValueError("'raster_width' must be > 0") + raster_width = np.clip(raster_width, None, self._trace.length) + num_raster_segments = int(np.round(self._trace.length / raster_width)) raster_width_eff = self._trace.length / num_raster_segments locations = np.arange(0, diff --git a/tests/test_point_cloud_generator.py b/tests/test_point_cloud_generator.py index 4809170..d178dff 100644 --- a/tests/test_point_cloud_generator.py +++ b/tests/test_point_cloud_generator.py @@ -783,8 +783,32 @@ def test_geometry_rasterization_trace(): assert math.isclose(exp_point_distance[j - 2], point_distance) + # check if raster width is clipped to valid range ----- + data = geometry.rasterize(7, 1000) + + assert data.shape[1] == 12 + + for i in range(12): + if i < 6: + print(data[:, i]) + assert data[0, i] == 0 + else: + print(data[:, i]) + assert data[1, i] == 1 -test_geometry_rasterization_trace() + # exceptions ------------------------------------------ + with pytest.raises(Exception): + geometry.rasterize(0, 1) + with pytest.raises(Exception): + geometry.rasterize(1, 0) + with pytest.raises(Exception): + geometry.rasterize(0, 0) + with pytest.raises(Exception): + geometry.rasterize(-2.3, 1) + with pytest.raises(Exception): + geometry.rasterize(1, -4.6) + with pytest.raises(Exception): + geometry.rasterize(-2.3, -4.6) def test_geometry_rasterization_profile_interpolation(): From 27a6401bb2e0f95ae0a6cd26037d18e71ff6e84f Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Wed, 5 Feb 2020 09:39:06 +0100 Subject: [PATCH 140/177] Fix a bug --- tests/test_point_cloud_generator.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/test_point_cloud_generator.py b/tests/test_point_cloud_generator.py index d178dff..cfb1b5a 100644 --- a/tests/test_point_cloud_generator.py +++ b/tests/test_point_cloud_generator.py @@ -790,11 +790,9 @@ def test_geometry_rasterization_trace(): for i in range(12): if i < 6: - print(data[:, i]) - assert data[0, i] == 0 + math.isclose(data[0, i], 0) else: - print(data[:, i]) - assert data[1, i] == 1 + assert math.isclose(data[1, i], 1) # exceptions ------------------------------------------ with pytest.raises(Exception): From 8040997f81588e81ebdf3793b28062a859c9ce1c Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Wed, 5 Feb 2020 09:52:07 +0100 Subject: [PATCH 141/177] Merge 2 packages --- mypackage/geometry.py | 666 +++++++++++++++++++++ mypackage/point_cloud_generator.py | 672 --------------------- tests/test_geometry.py | 855 ++++++++++++++++++++++++++- tests/test_point_cloud_generator.py | 870 ---------------------------- 4 files changed, 1520 insertions(+), 1543 deletions(-) delete mode 100644 mypackage/point_cloud_generator.py delete mode 100644 tests/test_point_cloud_generator.py diff --git a/mypackage/geometry.py b/mypackage/geometry.py index 3d36c92..03691ca 100644 --- a/mypackage/geometry.py +++ b/mypackage/geometry.py @@ -632,3 +632,669 @@ def rasterize(self, raster_width): last_point = self.segments[-1].point_end[:, np.newaxis] raster_data = np.hstack((raster_data, last_point)) return raster_data + + +# Profile class --------------------------------------------------------------- + +class Profile: + """Defines a 2d profile.""" + + def __init__(self, shapes): + """ + Construct profile class. + + :param: shapes: Instance or list of geo.Shape class(es) + """ + self._shapes = [] + self.add_shapes(shapes) + + @property + def num_shapes(self): + """ + Get the number of shapes of the profile. + + :return: Number of shapes + """ + return len(self._shapes) + + def add_shapes(self, shapes): + """ + Add shapes to the profile. + + :param shapes: Instance or list of geo.Shape class(es) + :return: --- + """ + if not isinstance(shapes, list): + shapes = [shapes] + + if not all(isinstance(shape, Shape) for shape in shapes): + raise TypeError( + "Only instances or lists of Shape objects are accepted.") + + self._shapes += shapes + + def rasterize(self, raster_width): + """ + Rasterize the profile. + + :param: raster_width: Raster width + :return: Raster data + """ + raster_data = np.empty([2, 0]) + for shape in self._shapes: + raster_data = np.hstack( + (raster_data, shape.rasterize(raster_width))) + + return raster_data + + @property + def shapes(self): + """ + Get the profiles shapes. + + :return: Shapes + """ + return self._shapes + + +# Trace segment classes ------------------------------------------------------- + +class LinearHorizontalTraceSegment: + """Trace segment with a linear path and constant z-component.""" + + def __init__(self, length): + """ + Constructor. + + :param length: Length of the segment + """ + if length <= 0: + raise ValueError("'length' must have a positive value.") + self._length = float(length) + + @property + def length(self): + """ + Get the length of the segment. + + :return: Length of the segment + """ + return self._length + + def local_coordinate_system(self, relative_position): + """ + Calculate a local coordinate system along the trace segment. + + :param relative_position: Relative position on the trace [0 .. 1] + :return: Local coordinate system + """ + relative_position = np.clip(relative_position, 0, 1) + + origin = np.array([1, 0, 0]) * relative_position * self._length + return tf.CoordinateSystem(origin=origin) + + +class RadialHorizontalTraceSegment: + """Trace segment describing an arc with constant z-component.""" + + def __init__(self, radius, angle, clockwise=False): + """ + Constructor. + + :param radius: Radius of the arc + :param angle: Angle of the arc + :param clockwise: If True, the rotation is clockwise. Otherwise it + is counter-clockwise. + """ + if radius <= 0: + raise ValueError("'radius' must have a positive value.") + if angle <= 0: + raise ValueError("'angle' must have a positive value.") + self._radius = float(radius) + self._angle = float(angle) + self._length = self._arc_length(self.radius, self.angle) + if clockwise: + self._sign_winding = -1 + else: + self._sign_winding = 1 + + @staticmethod + def _arc_length(radius, angle): + """ + Calculate the arc length. + + :param radius: Radius + :param angle: Angle (rad) + :return: Arc length + """ + return angle * radius + + @property + def angle(self): + """ + Get the angle of the segment. + + :return: Angle of the segment + """ + return self._angle + + @property + def length(self): + """ + Get the length of the segment. + + :return: Length of the segment + """ + return self._length + + @property + def radius(self): + """ + Get the radius of the segment. + + :return: Radius of the segment + """ + return self._radius + + @property + def is_clockwise(self): + """ + Get True, if the segments winding is clockwise, False otherwise. + + :return: True or False + """ + return self._sign_winding < 0 + + def local_coordinate_system(self, relative_position): + """ + Calculate a local coordinate system along the trace segment. + + :param relative_position: Relative position on the trace [0 .. 1] + :return: Local coordinate system + """ + relative_position = np.clip(relative_position, 0, 1) + + basis = tf.rotation_matrix_z( + self._angle * relative_position * self._sign_winding) + translation = np.array([0, -1, 0]) * self._radius * self._sign_winding + + origin = np.matmul(basis, translation) - translation + return tf.CoordinateSystem(basis, origin) + + +# Trace class ----------------------------------------------------------------- + +class Trace: + """Defines a 3d trace.""" + + def __init__(self, segments, coordinate_system=tf.CoordinateSystem()): + """ + Constructor. + + :param segments: Single segment or list of segments + :param coordinate_system: Coordinate system of the trace + """ + if not isinstance(coordinate_system, tf.CoordinateSystem): + raise TypeError( + "'coordinate_system' must be of type " + "'transformations.CoordinateSystem'") + + self._segments = ut.to_list(segments) + self._create_lookups(coordinate_system) + + if self.length <= 0: + raise Exception("Trace has no length.") + + def _create_lookups(self, coordinate_system_start): + """ + Create lookup tables. + + :param coordinate_system_start: Coordinate system at the start of + the trace. + :return: --- + """ + self._coordinate_system_lookup = [coordinate_system_start] + self._total_length_lookup = [0] + self._segment_length_lookup = [] + + segments = self._segments + + total_length = 0 + for i, segment in enumerate(segments): + # Fill coordinate system lookup + lcs_segment_end = segments[i].local_coordinate_system(1) + csys = self._coordinate_system_lookup[i] + lcs_segment_end + self._coordinate_system_lookup += [csys] + + # Fill length lookups + segment_length = segment.length + total_length += segment_length + self._segment_length_lookup += [segment_length] + self._total_length_lookup += [total_length] + + def _get_segment_index(self, position): + """ + Get the segment index for a certain position. + + :param position: Position + :return: Segment index + """ + position = np.clip(position, 0, self.length) + for i in range(len(self._total_length_lookup) - 2): + if position <= self._total_length_lookup[i + 1]: + return i + return self.num_segments - 1 + + @property + def coordinate_system(self): + """ + Get the trace's coordinate system. + + :return: Coordinate system of the trace + """ + return self._coordinate_system_lookup[0] + + @property + def length(self): + """ + Get the length of the trace. + + :return: Length of the trace. + """ + return self._total_length_lookup[-1] + + @property + def segments(self): + """ + Get the trace's segments. + + :return: Segments of the trace + """ + return self._segments + + @property + def num_segments(self): + """ + Get the number of segments. + + :return: Number of segments + """ + return len(self._segments) + + def local_coordinate_system(self, position): + """ + Get the local coordinate system at a specific position on the trace. + + :param position: Position + :return: Local coordinate system + """ + idx = self._get_segment_index(position) + + total_length_start = self._total_length_lookup[idx] + segment_length = self._segment_length_lookup[idx] + weight = (position - total_length_start) / segment_length + + local_segment_cs = self.segments[idx].local_coordinate_system(weight) + segment_start_cs = self._coordinate_system_lookup[idx] + + return segment_start_cs + local_segment_cs + + def rasterize(self, raster_width): + """ + Rasterize the trace. + + :return: Raster data + """ + if not raster_width > 0: + raise ValueError("'raster_width' must be > 0") + + raster_width = np.clip(raster_width, 0, self.length) + num_raster_segments = int(np.round(self.length / raster_width)) + raster_width_eff = self.length / num_raster_segments + + idx = 0 + raster_data = np.empty((3, 0)) + for i in range(num_raster_segments): + location = i * raster_width_eff + while not location <= self._total_length_lookup[idx + 1]: + idx += 1 + + segment_location = location - self._total_length_lookup[idx] + weight = segment_location / self._segment_length_lookup[idx] + + local_segment_cs = self.segments[idx].local_coordinate_system( + weight) + segment_start_cs = self._coordinate_system_lookup[idx] + + local_cs = segment_start_cs + local_segment_cs + + data_point = local_cs.origin[:, np.newaxis] + raster_data = np.hstack([raster_data, data_point]) + + last_point = self._coordinate_system_lookup[-1].origin[:, np.newaxis] + return np.hstack([raster_data, last_point]) + + +# Linear profile interpolation class ------------------------------------------ + +class LinearProfileInterpolationSBS: + """Linear segment by segment interpolation class for profiles.""" + + @staticmethod + def interpolate(profile_a, profile_b, weight): + """ + Interpolate 2 profiles. + + :param profile_a: First profile + :param profile_b: Second profile + :param weight: Weighting factor [0 .. 1]. If 0, the profile is + identical to 'a' and if 1, it is identical to b. + :return: Interpolated profile + """ + weight = np.clip(weight, 0, 1) + if not len(profile_a.shapes) == len(profile_b.shapes): + raise Exception("Number of profile shapes do not match.") + + shapes_c = [] + for i in range(profile_a.num_shapes): + shapes_c += [Shape.linear_interpolation(profile_a.shapes[i], + profile_b.shapes[i], + weight)] + + return Profile(shapes_c) + + +# Varying profile class ------------------------------------------------------- + +class VariableProfile: + """Class to define a profile of variable shape.""" + + def __init__(self, profiles, locations, interpolation_schemes): + """ + Constructor. + + :param profiles: List of profiles. + :param locations: Ascending list of profile locations. Since the + first location needs to be 0, it can be omitted. + :param interpolation_schemes: List of interpolation schemes to + define the interpolation between two locations. + """ + locations = ut.to_list(locations) + interpolation_schemes = ut.to_list(interpolation_schemes) + + if not locations[0] == 0: + locations = [0] + locations + + if not len(profiles) == len(locations): + raise Exception( + "Invalid list of locations. See function description.") + + if not len(interpolation_schemes) == len(profiles) - 1: + raise Exception( + "Number of interpolations must be 1 less than number of " + "profiles.") + + for i in range(len(profiles) - 1): + if locations[i] >= locations[i + 1]: + raise Exception( + "Locations need to be sorted in ascending order.") + + self._profiles = profiles + self._locations = locations + self._interpolation_schemes = interpolation_schemes + + def _segment_index(self, location): + """ + Get the index of the segment at a certain location. + + :param location: Location + :return: Segment index + """ + idx = 0 + while location > self._locations[idx + 1]: + idx += 1 + return idx + + @property + def interpolation_schemes(self): + """ + Get the interpolation schemes. + + :return: List of interpolation schemes + """ + return self._interpolation_schemes + + @property + def locations(self): + """ + Get the locations. + + :return: List of locations + """ + return self._locations + + @property + def max_location(self): + """ + Get the maximum location. + + :return: Maximum location + """ + return self._locations[-1] + + @property + def num_interpolation_schemes(self): + """ + Get the number of interpolation schemes. + + :return: Number of interpolation schemes + """ + return len(self._interpolation_schemes) + + @property + def num_locations(self): + """ + Get the number of profile locations. + + :return: Number of profile locations + """ + return len(self._locations) + + @property + def num_profiles(self): + """ + Get the number of profiles. + + :return: Number of profiles + """ + return len(self._profiles) + + @property + def profiles(self): + """ + Get the profiles. + + :return: List of profiles + """ + return self._profiles + + def local_profile(self, location): + """ + Get the profile at the specified location. + + :param location: Location + :return: Local profile. + """ + location = np.clip(location, 0, self.max_location) + + idx = self._segment_index(location) + segment_length = self._locations[idx + 1] - self._locations[idx] + weight = (location - self._locations[idx]) / segment_length + + return self._interpolation_schemes[idx].interpolate( + self._profiles[idx], self._profiles[idx + 1], weight) + + +# Geometry class ------------------------------------------------------------- + +class Geometry: + """Define the experimental geometry.""" + + def __init__(self, profile, trace): + """ + Constructor. + + :param profile: Constant or variable profile. + :param trace: Trace + """ + self._check_inputs(profile, trace) + self._profile = profile + self._trace = trace + + @staticmethod + def _check_inputs(profile, trace): + """ + Check the inputs to the constructor. + + :param profile: Constant or variable profile. + :param trace: Trace + :return: --- + """ + if not isinstance(profile, (Profile, VariableProfile)): + raise TypeError( + "'profile' must be a 'Profile' or 'VariableProfile' class") + + if not isinstance(trace, Trace): + raise TypeError( + "'trace' must be a 'Trace' class") + + def _get_local_profile_data(self, trace_location, raster_width): + """ + Get a rasterized profile at a certain location on the trace. + + :param trace_location: Location on the trace + :param raster_width: Raster width + :return: + """ + relative_location = trace_location / self._trace.length + profile_location = relative_location * self._profile.max_location + profile = self._profile.local_profile(profile_location) + return self._profile_raster_data_3d(profile, raster_width) + + def _rasterize_trace(self, raster_width): + """ + Rasterize the trace. + + :param raster_width: Raster width + :return: Raster data + """ + if not raster_width > 0: + raise ValueError("'raster_width' must be > 0") + raster_width = np.clip(raster_width, None, self._trace.length) + + num_raster_segments = int(np.round(self._trace.length / raster_width)) + raster_width_eff = self._trace.length / num_raster_segments + locations = np.arange(0, + self._trace.length - raster_width_eff / 2, + raster_width_eff) + return np.hstack([locations, self._trace.length]) + + def _get_transformed_profile_data(self, profile_raster_data, location): + """ + Transform a profiles data to a specified location on the trace. + + :param profile_raster_data: Rasterized profile + :param location: Location on the trace + :return: Transformed profile data + """ + local_cs = self._trace.local_coordinate_system(location) + local_data = np.matmul(local_cs.basis, profile_raster_data) + return local_data + local_cs.origin[:, np.newaxis] + + @staticmethod + def _profile_raster_data_3d(profile, raster_width): + """ + Get the rasterized profile in 3d. + + The profile is located in the x-z-plane. + + :param profile: Profile + :param raster_width: Raster width + :return: Rasterized profile in 3d + """ + profile_data = profile.rasterize(raster_width) + return np.insert(profile_data, 0, 0, axis=0) + + def _rasterize_constant_profile(self, profile_raster_width, + trace_raster_width): + """ + Rasterize the geometry with a constant profile. + + :param profile_raster_width: Raster width of the profiles + :param trace_raster_width: Distance between two profiles + :return: Raster data + """ + profile_data = self._profile_raster_data_3d(self._profile, + profile_raster_width) + + locations = self._rasterize_trace(trace_raster_width) + raster_data = np.empty([3, 0]) + for _, location in enumerate(locations): + local_data = self._get_transformed_profile_data(profile_data, + location) + raster_data = np.hstack([raster_data, local_data]) + + return raster_data + + def _rasterize_variable_profile(self, profile_raster_width, + trace_raster_width): + """ + Rasterize the geometry with a variable profile. + + :param profile_raster_width: Raster width of the profiles + :param trace_raster_width: Distance between two profiles + :return: Raster data + """ + locations = self._rasterize_trace(trace_raster_width) + raster_data = np.empty([3, 0]) + for _, location in enumerate(locations): + profile_data = self._get_local_profile_data(location, + profile_raster_width) + + local_data = self._get_transformed_profile_data(profile_data, + location) + raster_data = np.hstack([raster_data, local_data]) + + return raster_data + + @property + def profile(self): + """ + Get the geometry's profile. + + :return: Profile + """ + return self._profile + + @property + def trace(self): + """ + Get the geometry's trace. + + :return: Trace + """ + return self._trace + + def rasterize(self, profile_raster_width, trace_raster_width): + """ + Rasterize the geometry. + + :param profile_raster_width: Raster width of the profiles + :param trace_raster_width: Distance between two profiles + :return: Raster data + """ + if isinstance(self._profile, Profile): + return self._rasterize_constant_profile(profile_raster_width, + trace_raster_width) + return self._rasterize_variable_profile(profile_raster_width, + trace_raster_width) diff --git a/mypackage/point_cloud_generator.py b/mypackage/point_cloud_generator.py deleted file mode 100644 index a22d52a..0000000 --- a/mypackage/point_cloud_generator.py +++ /dev/null @@ -1,672 +0,0 @@ -"""Contains methods and classes to generate 3d point clouds.""" - -import numpy as np -import mypackage._utility as utils -import mypackage.geometry as geo -import mypackage.transformations as tf - - -# Profile class --------------------------------------------------------------- - -class Profile: - """Defines a 2d profile.""" - - def __init__(self, shapes): - """ - Construct profile class. - - :param: shapes: Instance or list of geo.Shape class(es) - """ - self._shapes = [] - self.add_shapes(shapes) - - @property - def num_shapes(self): - """ - Get the number of shapes of the profile. - - :return: Number of shapes - """ - return len(self._shapes) - - def add_shapes(self, shapes): - """ - Add shapes to the profile. - - :param shapes: Instance or list of geo.Shape class(es) - :return: --- - """ - if not isinstance(shapes, list): - shapes = [shapes] - - if not all(isinstance(shape, geo.Shape) for shape in shapes): - raise TypeError( - "Only instances or lists of Shape objects are accepted.") - - self._shapes += shapes - - def rasterize(self, raster_width): - """ - Rasterize the profile. - - :param: raster_width: Raster width - :return: Raster data - """ - raster_data = np.empty([2, 0]) - for shape in self._shapes: - raster_data = np.hstack( - (raster_data, shape.rasterize(raster_width))) - - return raster_data - - @property - def shapes(self): - """ - Get the profiles shapes. - - :return: Shapes - """ - return self._shapes - - -# Trace segment classes ------------------------------------------------------- - -class LinearHorizontalTraceSegment: - """Trace segment with a linear path and constant z-component.""" - - def __init__(self, length): - """ - Constructor. - - :param length: Length of the segment - """ - if length <= 0: - raise ValueError("'length' must have a positive value.") - self._length = float(length) - - @property - def length(self): - """ - Get the length of the segment. - - :return: Length of the segment - """ - return self._length - - def local_coordinate_system(self, relative_position): - """ - Calculate a local coordinate system along the trace segment. - - :param relative_position: Relative position on the trace [0 .. 1] - :return: Local coordinate system - """ - relative_position = np.clip(relative_position, 0, 1) - - origin = np.array([1, 0, 0]) * relative_position * self._length - return tf.CoordinateSystem(origin=origin) - - -class RadialHorizontalTraceSegment: - """Trace segment describing an arc with constant z-component.""" - - def __init__(self, radius, angle, clockwise=False): - """ - Constructor. - - :param radius: Radius of the arc - :param angle: Angle of the arc - :param clockwise: If True, the rotation is clockwise. Otherwise it - is counter-clockwise. - """ - if radius <= 0: - raise ValueError("'radius' must have a positive value.") - if angle <= 0: - raise ValueError("'angle' must have a positive value.") - self._radius = float(radius) - self._angle = float(angle) - self._length = self._arc_length(self.radius, self.angle) - if clockwise: - self._sign_winding = -1 - else: - self._sign_winding = 1 - - @staticmethod - def _arc_length(radius, angle): - """ - Calculate the arc length. - - :param radius: Radius - :param angle: Angle (rad) - :return: Arc length - """ - return angle * radius - - @property - def angle(self): - """ - Get the angle of the segment. - - :return: Angle of the segment - """ - return self._angle - - @property - def length(self): - """ - Get the length of the segment. - - :return: Length of the segment - """ - return self._length - - @property - def radius(self): - """ - Get the radius of the segment. - - :return: Radius of the segment - """ - return self._radius - - @property - def is_clockwise(self): - """ - Get True, if the segments winding is clockwise, False otherwise. - - :return: True or False - """ - return self._sign_winding < 0 - - def local_coordinate_system(self, relative_position): - """ - Calculate a local coordinate system along the trace segment. - - :param relative_position: Relative position on the trace [0 .. 1] - :return: Local coordinate system - """ - relative_position = np.clip(relative_position, 0, 1) - - basis = tf.rotation_matrix_z( - self._angle * relative_position * self._sign_winding) - translation = np.array([0, -1, 0]) * self._radius * self._sign_winding - - origin = np.matmul(basis, translation) - translation - return tf.CoordinateSystem(basis, origin) - - -# Trace class ----------------------------------------------------------------- - -class Trace: - """Defines a 3d trace.""" - - def __init__(self, segments, coordinate_system=tf.CoordinateSystem()): - """ - Constructor. - - :param segments: Single segment or list of segments - :param coordinate_system: Coordinate system of the trace - """ - if not isinstance(coordinate_system, tf.CoordinateSystem): - raise TypeError( - "'coordinate_system' must be of type " - "'transformations.CoordinateSystem'") - - self._segments = utils.to_list(segments) - self._create_lookups(coordinate_system) - - if self.length <= 0: - raise Exception("Trace has no length.") - - def _create_lookups(self, coordinate_system_start): - """ - Create lookup tables. - - :param coordinate_system_start: Coordinate system at the start of - the trace. - :return: --- - """ - self._coordinate_system_lookup = [coordinate_system_start] - self._total_length_lookup = [0] - self._segment_length_lookup = [] - - segments = self._segments - - total_length = 0 - for i, segment in enumerate(segments): - # Fill coordinate system lookup - lcs_segment_end = segments[i].local_coordinate_system(1) - csys = self._coordinate_system_lookup[i] + lcs_segment_end - self._coordinate_system_lookup += [csys] - - # Fill length lookups - segment_length = segment.length - total_length += segment_length - self._segment_length_lookup += [segment_length] - self._total_length_lookup += [total_length] - - def _get_segment_index(self, position): - """ - Get the segment index for a certain position. - - :param position: Position - :return: Segment index - """ - position = np.clip(position, 0, self.length) - for i in range(len(self._total_length_lookup) - 2): - if position <= self._total_length_lookup[i + 1]: - return i - return self.num_segments - 1 - - @property - def coordinate_system(self): - """ - Get the trace's coordinate system. - - :return: Coordinate system of the trace - """ - return self._coordinate_system_lookup[0] - - @property - def length(self): - """ - Get the length of the trace. - - :return: Length of the trace. - """ - return self._total_length_lookup[-1] - - @property - def segments(self): - """ - Get the trace's segments. - - :return: Segments of the trace - """ - return self._segments - - @property - def num_segments(self): - """ - Get the number of segments. - - :return: Number of segments - """ - return len(self._segments) - - def local_coordinate_system(self, position): - """ - Get the local coordinate system at a specific position on the trace. - - :param position: Position - :return: Local coordinate system - """ - idx = self._get_segment_index(position) - - total_length_start = self._total_length_lookup[idx] - segment_length = self._segment_length_lookup[idx] - weight = (position - total_length_start) / segment_length - - local_segment_cs = self.segments[idx].local_coordinate_system(weight) - segment_start_cs = self._coordinate_system_lookup[idx] - - return segment_start_cs + local_segment_cs - - def rasterize(self, raster_width): - """ - Rasterize the trace. - - :return: Raster data - """ - if not raster_width > 0: - raise ValueError("'raster_width' must be > 0") - - raster_width = np.clip(raster_width, 0, self.length) - num_raster_segments = int(np.round(self.length / raster_width)) - raster_width_eff = self.length / num_raster_segments - - idx = 0 - raster_data = np.empty((3, 0)) - for i in range(num_raster_segments): - location = i * raster_width_eff - while not location <= self._total_length_lookup[idx + 1]: - idx += 1 - - segment_location = location - self._total_length_lookup[idx] - weight = segment_location / self._segment_length_lookup[idx] - - local_segment_cs = self.segments[idx].local_coordinate_system( - weight) - segment_start_cs = self._coordinate_system_lookup[idx] - - local_cs = segment_start_cs + local_segment_cs - - data_point = local_cs.origin[:, np.newaxis] - raster_data = np.hstack([raster_data, data_point]) - - last_point = self._coordinate_system_lookup[-1].origin[:, np.newaxis] - return np.hstack([raster_data, last_point]) - - -# Linear profile interpolation class ------------------------------------------ - -class LinearProfileInterpolationSBS: - """Linear segment by segment interpolation class for profiles.""" - - @staticmethod - def interpolate(profile_a, profile_b, weight): - """ - Interpolate 2 profiles. - - :param profile_a: First profile - :param profile_b: Second profile - :param weight: Weighting factor [0 .. 1]. If 0, the profile is - identical to 'a' and if 1, it is identical to b. - :return: Interpolated profile - """ - weight = np.clip(weight, 0, 1) - if not len(profile_a.shapes) == len(profile_b.shapes): - raise Exception("Number of profile shapes do not match.") - - shapes_c = [] - for i in range(profile_a.num_shapes): - shapes_c += [geo.Shape.linear_interpolation(profile_a.shapes[i], - profile_b.shapes[i], - weight)] - - return Profile(shapes_c) - - -# Varying profile class ------------------------------------------------------- - -class VariableProfile: - """Class to define a profile of variable shape.""" - - def __init__(self, profiles, locations, interpolation_schemes): - """ - Constructor. - - :param profiles: List of profiles. - :param locations: Ascending list of profile locations. Since the - first location needs to be 0, it can be omitted. - :param interpolation_schemes: List of interpolation schemes to - define the interpolation between two locations. - """ - locations = utils.to_list(locations) - interpolation_schemes = utils.to_list(interpolation_schemes) - - if not locations[0] == 0: - locations = [0] + locations - - if not len(profiles) == len(locations): - raise Exception( - "Invalid list of locations. See function description.") - - if not len(interpolation_schemes) == len(profiles) - 1: - raise Exception( - "Number of interpolations must be 1 less than number of " - "profiles.") - - for i in range(len(profiles) - 1): - if locations[i] >= locations[i + 1]: - raise Exception( - "Locations need to be sorted in ascending order.") - - self._profiles = profiles - self._locations = locations - self._interpolation_schemes = interpolation_schemes - - def _segment_index(self, location): - """ - Get the index of the segment at a certain location. - - :param location: Location - :return: Segment index - """ - idx = 0 - while location > self._locations[idx + 1]: - idx += 1 - return idx - - @property - def interpolation_schemes(self): - """ - Get the interpolation schemes. - - :return: List of interpolation schemes - """ - return self._interpolation_schemes - - @property - def locations(self): - """ - Get the locations. - - :return: List of locations - """ - return self._locations - - @property - def max_location(self): - """ - Get the maximum location. - - :return: Maximum location - """ - return self._locations[-1] - - @property - def num_interpolation_schemes(self): - """ - Get the number of interpolation schemes. - - :return: Number of interpolation schemes - """ - return len(self._interpolation_schemes) - - @property - def num_locations(self): - """ - Get the number of profile locations. - - :return: Number of profile locations - """ - return len(self._locations) - - @property - def num_profiles(self): - """ - Get the number of profiles. - - :return: Number of profiles - """ - return len(self._profiles) - - @property - def profiles(self): - """ - Get the profiles. - - :return: List of profiles - """ - return self._profiles - - def local_profile(self, location): - """ - Get the profile at the specified location. - - :param location: Location - :return: Local profile. - """ - location = np.clip(location, 0, self.max_location) - - idx = self._segment_index(location) - segment_length = self._locations[idx + 1] - self._locations[idx] - weight = (location - self._locations[idx]) / segment_length - - return self._interpolation_schemes[idx].interpolate( - self._profiles[idx], self._profiles[idx + 1], weight) - - -# Geometry class ------------------------------------------------------------- - -class Geometry: - """Define the experimental geometry.""" - - def __init__(self, profile, trace): - """ - Constructor. - - :param profile: Constant or variable profile. - :param trace: Trace - """ - self._check_inputs(profile, trace) - self._profile = profile - self._trace = trace - - @staticmethod - def _check_inputs(profile, trace): - """ - Check the inputs to the constructor. - - :param profile: Constant or variable profile. - :param trace: Trace - :return: --- - """ - if not isinstance(profile, (Profile, VariableProfile)): - raise TypeError( - "'profile' must be a 'Profile' or 'VariableProfile' class") - - if not isinstance(trace, Trace): - raise TypeError( - "'trace' must be a 'Trace' class") - - def _get_local_profile_data(self, trace_location, raster_width): - """ - Get a rasterized profile at a certain location on the trace. - - :param trace_location: Location on the trace - :param raster_width: Raster width - :return: - """ - relative_location = trace_location / self._trace.length - profile_location = relative_location * self._profile.max_location - profile = self._profile.local_profile(profile_location) - return self._profile_raster_data_3d(profile, raster_width) - - def _rasterize_trace(self, raster_width): - """ - Rasterize the trace. - - :param raster_width: Raster width - :return: Raster data - """ - if not raster_width > 0: - raise ValueError("'raster_width' must be > 0") - raster_width = np.clip(raster_width, None, self._trace.length) - - num_raster_segments = int(np.round(self._trace.length / raster_width)) - raster_width_eff = self._trace.length / num_raster_segments - locations = np.arange(0, - self._trace.length - raster_width_eff / 2, - raster_width_eff) - return np.hstack([locations, self._trace.length]) - - def _get_transformed_profile_data(self, profile_raster_data, location): - """ - Transform a profiles data to a specified location on the trace. - - :param profile_raster_data: Rasterized profile - :param location: Location on the trace - :return: Transformed profile data - """ - local_cs = self._trace.local_coordinate_system(location) - local_data = np.matmul(local_cs.basis, profile_raster_data) - return local_data + local_cs.origin[:, np.newaxis] - - @staticmethod - def _profile_raster_data_3d(profile, raster_width): - """ - Get the rasterized profile in 3d. - - The profile is located in the x-z-plane. - - :param profile: Profile - :param raster_width: Raster width - :return: Rasterized profile in 3d - """ - profile_data = profile.rasterize(raster_width) - return np.insert(profile_data, 0, 0, axis=0) - - def _rasterize_constant_profile(self, profile_raster_width, - trace_raster_width): - """ - Rasterize the geometry with a constant profile. - - :param profile_raster_width: Raster width of the profiles - :param trace_raster_width: Distance between two profiles - :return: Raster data - """ - profile_data = self._profile_raster_data_3d(self._profile, - profile_raster_width) - - locations = self._rasterize_trace(trace_raster_width) - raster_data = np.empty([3, 0]) - for _, location in enumerate(locations): - local_data = self._get_transformed_profile_data(profile_data, - location) - raster_data = np.hstack([raster_data, local_data]) - - return raster_data - - def _rasterize_variable_profile(self, profile_raster_width, - trace_raster_width): - """ - Rasterize the geometry with a variable profile. - - :param profile_raster_width: Raster width of the profiles - :param trace_raster_width: Distance between two profiles - :return: Raster data - """ - locations = self._rasterize_trace(trace_raster_width) - raster_data = np.empty([3, 0]) - for _, location in enumerate(locations): - profile_data = self._get_local_profile_data(location, - profile_raster_width) - - local_data = self._get_transformed_profile_data(profile_data, - location) - raster_data = np.hstack([raster_data, local_data]) - - return raster_data - - @property - def profile(self): - """ - Get the geometry's profile. - - :return: Profile - """ - return self._profile - - @property - def trace(self): - """ - Get the geometry's trace. - - :return: Trace - """ - return self._trace - - def rasterize(self, profile_raster_width, trace_raster_width): - """ - Rasterize the geometry. - - :param profile_raster_width: Raster width of the profiles - :param trace_raster_width: Distance between two profiles - :return: Raster data - """ - if isinstance(self._profile, Profile): - return self._rasterize_constant_profile(profile_raster_width, - trace_raster_width) - return self._rasterize_variable_profile(profile_raster_width, - trace_raster_width) diff --git a/tests/test_geometry.py b/tests/test_geometry.py index 42ae299..9fd9d4e 100644 --- a/tests/test_geometry.py +++ b/tests/test_geometry.py @@ -1,5 +1,7 @@ -import mypackage._utility as utils import mypackage.geometry as geo +import mypackage.transformations as tf +import mypackage._utility as utils + import tests._helpers as helpers import pytest @@ -19,6 +21,52 @@ def check_segments_identical(a, b): helpers.check_vectors_identical(a.point_center, b.point_center) +def check_profiles_identical(a, b): + assert a.num_shapes == b.num_shapes + for i in range(a.num_shapes): + check_shapes_identical(a.shapes[i], b.shapes[i]) + + +def check_shapes_identical(a, b): + assert a.num_segments == b.num_segments + for i in range(a.num_segments): + check_segments_identical(a.segments[i], b.segments[i]) + + +def check_trace_segments_identical(a, b): + assert isinstance(a, type(b)) + if isinstance(a, geo.LinearHorizontalTraceSegment): + assert a.length == b.length + else: + assert a.is_clockwise == b.is_clockwise + assert math.isclose(a.angle, b.angle) + assert math.isclose(a.length, b.length) + assert math.isclose(a.radius, b.radius) + + +def check_traces_identical(a, b): + assert a.num_segments == b.num_segments + for i in range(a.num_segments): + check_trace_segments_identical(a.segments[i], b.segments[i]) + + +def get_default_profiles(): + a_0 = [0, 0] + a_1 = [8, 16] + a_2 = [16, 0] + shape_a01 = geo.Shape(geo.LineSegment.construct_with_points(a_0, a_1)) + shape_a12 = geo.Shape(geo.LineSegment.construct_with_points(a_1, a_2)) + profile_a = geo.Profile([shape_a01, shape_a12]) + + b_0 = [-4, 8] + b_1 = [0, 8] + b_2 = [16, -16] + shape_b01 = geo.Shape(geo.LineSegment.construct_with_points(b_0, b_1)) + shape_b12 = geo.Shape(geo.LineSegment.construct_with_points(b_1, b_2)) + profile_b = geo.Profile([shape_b01, shape_b12]) + return [profile_a, profile_b] + + # helper for segment tests ---------------------------------------------------- def default_segment_rasterization_tests(segment, raster_width, point_start, @@ -930,3 +978,808 @@ def test_shape_linear_interpolation(): shape_a.add_segments(geo.LineSegment.construct_with_points([2, 0], [2, 2])) with pytest.raises(Exception): geo.Shape.linear_interpolation(shape_a, shape_b, 0.25) + + +# Test profile class ---------------------------------------------------------- + +def test_profile_construction_and_shape_addition(): + segment0 = geo.LineSegment.construct_with_points([0, 0], [1, 0]) + segment1 = geo.LineSegment.construct_with_points([1, 0], [2, -1]) + segment2 = geo.LineSegment.construct_with_points([2, -1], [0, -1]) + + shape = geo.Shape([segment0, segment1, segment2]) + + # Check invalid types + with pytest.raises(TypeError): + geo.Profile(3) + with pytest.raises(TypeError): + geo.Profile("This is not right") + with pytest.raises(TypeError): + geo.Profile([2, 8, 1]) + + # Check valid types + profile = geo.Profile(shape) + assert profile.num_shapes == 1 + profile = geo.Profile([shape, shape]) + assert profile.num_shapes == 2 + + # Check invalid addition + with pytest.raises(TypeError): + profile.add_shapes([shape, 0.1]) + with pytest.raises(TypeError): + profile.add_shapes(["shape"]) + with pytest.raises(TypeError): + profile.add_shapes(0.1) + + # Check that invalid calls only raise an exception and do not invalidate + # the internal data + assert profile.num_shapes == 2 + + # Check valid addition + profile.add_shapes(shape) + assert profile.num_shapes == 3 + profile.add_shapes([shape, shape]) + assert profile.num_shapes == 5 + + # Check shapes + shapes_profile = profile.shapes + for shape_profile in shapes_profile: + assert shape.num_segments == shape_profile.num_segments + + segments = shape.segments + segments_profile = shape_profile.segments + + assert len(segments) == shape.num_segments + assert len(segments) == len(segments_profile) + + for i in range(shape.num_segments): + assert isinstance(segments_profile[i], type(segments[i])) + points = segments[i].points + points_profile = segments_profile[i].points + for j in range(2): + helpers.check_vectors_identical(points[:, j], + points_profile[:, j]) + + +def test_profile_rasterization(): + raster_width = 0.1 + shape0 = geo.Shape( + geo.LineSegment.construct_with_points([-1, 0], [-raster_width, 0])) + shape1 = geo.Shape(geo.LineSegment.construct_with_points([0, 0], [1, 0])) + shape2 = geo.Shape( + geo.LineSegment.construct_with_points([1 + raster_width, 0], [2, 0])) + + profile = geo.Profile([shape0, shape1]) + profile.add_shapes(shape2) + + # rasterize + data = profile.rasterize(0.1) + + # no duplications + assert helpers.are_all_points_unique(data) + + # check raster data size + expected_number_raster_points = int(round(3 / raster_width)) + 1 + assert data.shape[1] == expected_number_raster_points + + # Check that all shapes are rasterized correct + for i in range(int(round(3 / raster_width)) + 1): + expected_raster_point_x = i * raster_width - 1 + assert data[0, i] - expected_raster_point_x < 1E-9 + assert data[1, i] == 0 + + # exceptions + with pytest.raises(Exception): + profile.rasterize(0) + with pytest.raises(Exception): + profile.rasterize(-3) + + +# Test trace segment classes -------------------------------------------------- + +def check_trace_segment_length(segment, tolerance=1E-9): + lcs = segment.local_coordinate_system(1) + length_numeric_prev = np.linalg.norm(lcs.origin) + + # calculate numerical length by linearization + num_segments = 2. + num_iterations = 20 + + # calculate numerical length with increasing number of segments until + # the rate of change between 2 calculations is small enough + for i in range(num_iterations): + length_numeric = 0 + increment = 1. / num_segments + + ccs0 = segment.local_coordinate_system(0) + for rel_pos in (np.arange(increment, 1. + increment / 2, increment)): + ccs1 = segment.local_coordinate_system(rel_pos) + length_numeric += np.linalg.norm(ccs1.origin - ccs0.origin) + ccs0 = copy.deepcopy(ccs1) + + relative_change = length_numeric / length_numeric_prev + + length_numeric_prev = copy.deepcopy(length_numeric) + num_segments *= 2 + + if math.isclose(relative_change, 1, abs_tol=tolerance / 10): + break + assert i < num_iterations - 1, "Segment length could not be " \ + "determined numerically" + + assert math.isclose(length_numeric, segment.length, abs_tol=tolerance) + + +def check_trace_segment_orientation(segment): + # The initial orientation of a segment must be [0, 1, 0] + lcs = segment.local_coordinate_system(0) + helpers.check_vectors_identical(lcs.basis[:, 1], np.array([0, 1, 0])) + + delta = 1E-9 + for rel_pos in np.arange(0.1, 1.01, 0.1): + lcs = segment.local_coordinate_system(rel_pos) + lcs_d = segment.local_coordinate_system(rel_pos - delta) + trace_direction_numerical = tf.normalize(lcs.origin - lcs_d.origin) + + # Check that the y-axis is always aligned with the trace's direction + helpers.check_vectors_identical(lcs.basis[:, 0], + trace_direction_numerical, 1E-6) + + +def default_trace_segment_tests(segment, tolerance_length=1E-9): + lcs = segment.local_coordinate_system(0) + + # test that function actually returns a coordinate system class + assert isinstance(lcs, tf.CoordinateSystem) + + # check that origin for weight 0 is at [0,0,0] + for i in range(3): + assert math.isclose(lcs.origin[i], 0) + + check_trace_segment_length(segment, tolerance_length) + check_trace_segment_orientation(segment) + + +def test_linear_horizontal_trace_segment(): + length = 7.13 + segment = geo.LinearHorizontalTraceSegment(length) + + # default tests + default_trace_segment_tests(segment) + + # getter tests + assert math.isclose(segment.length, length) + + # invalid inputs + with pytest.raises(ValueError): + geo.LinearHorizontalTraceSegment(0) + with pytest.raises(ValueError): + geo.LinearHorizontalTraceSegment(-4.61) + + +def test_radial_horizontal_trace_segment(): + radius = 4.74 + angle = np.pi / 1.23 + segment_cw = geo.RadialHorizontalTraceSegment(radius, angle, True) + segment_ccw = geo.RadialHorizontalTraceSegment(radius, angle, False) + + # default tests + default_trace_segment_tests(segment_cw, 1E-4) + default_trace_segment_tests(segment_ccw, 1E-4) + + # getter tests + assert math.isclose(segment_cw.angle, angle) + assert math.isclose(segment_ccw.angle, angle) + assert math.isclose(segment_cw.radius, radius) + assert math.isclose(segment_ccw.radius, radius) + assert segment_cw.is_clockwise + assert not segment_ccw.is_clockwise + + # check positions + for weight in np.arange(0.1, 1, 0.1): + current_angle = angle * weight + x_exp = np.sin(current_angle) * radius + y_exp = (1 - np.cos(current_angle)) * radius + + lcs_cw = segment_cw.local_coordinate_system(weight) + lcs_ccw = segment_ccw.local_coordinate_system(weight) + + assert math.isclose(lcs_cw.origin[0], x_exp) + assert math.isclose(lcs_cw.origin[1], -y_exp) + assert math.isclose(lcs_ccw.origin[0], x_exp) + assert math.isclose(lcs_ccw.origin[1], y_exp) + + # invalid inputs + with pytest.raises(ValueError): + geo.RadialHorizontalTraceSegment(0, np.pi) + with pytest.raises(ValueError): + geo.RadialHorizontalTraceSegment(-0.53, np.pi) + with pytest.raises(ValueError): + geo.RadialHorizontalTraceSegment(1, 0) + with pytest.raises(ValueError): + geo.RadialHorizontalTraceSegment(1, -np.pi) + + +# Test trace class ------------------------------------------------------------ + +def test_trace_construction(): + linear_segment = geo.LinearHorizontalTraceSegment(1) + radial_segment = geo.RadialHorizontalTraceSegment(1, np.pi) + ccs_origin = np.array([2, 3, -2]) + ccs = helpers.rotated_coordinate_system(origin=ccs_origin) + + # test single segment construction -------------------- + trace = geo.Trace(linear_segment, ccs) + assert math.isclose(trace.length, linear_segment.length) + assert trace.num_segments == 1 + + segments = trace.segments + assert len(segments) == 1 + assert isinstance(segments[0], type(linear_segment)) + assert math.isclose(linear_segment.length, segments[0].length) + + helpers.check_matrices_identical(ccs.basis, trace.coordinate_system.basis) + helpers.check_vectors_identical(ccs.origin, trace.coordinate_system.origin) + + # test multi segment construction --------------------- + trace = geo.Trace([radial_segment, linear_segment]) + assert math.isclose(trace.length, + linear_segment.length + radial_segment.length) + assert trace.num_segments == 2 + + segments = trace.segments + assert len(segments) == 2 + assert isinstance(segments[0], type(radial_segment)) + assert isinstance(segments[1], type(linear_segment)) + + assert math.isclose(radial_segment.radius, segments[0].radius) + assert math.isclose(radial_segment.angle, segments[0].angle) + assert math.isclose(radial_segment.is_clockwise, segments[0].is_clockwise) + assert math.isclose(linear_segment.length, segments[1].length) + + helpers.check_matrices_identical(np.identity(3), + trace.coordinate_system.basis) + helpers.check_vectors_identical(np.array([0, 0, 0]), + trace.coordinate_system.origin) + + # check invalid inputs -------------------------------- + with pytest.raises(TypeError): + geo.Trace(radial_segment, linear_segment) + with pytest.raises(TypeError): + geo.Trace(radial_segment, 2) + with pytest.raises(Exception): + geo.Trace(None) + + # check construction with custom segment -------------- + class CustomSegment(): + def __init__(self): + self.length = None + + @staticmethod + def local_coordinate_system(*args): + return tf.CoordinateSystem() + + custom_segment = CustomSegment() + custom_segment.length = 3 + geo.Trace(custom_segment) + + with pytest.raises(Exception): + custom_segment.length = -12 + geo.Trace(custom_segment) + with pytest.raises(Exception): + custom_segment.length = 0 + geo.Trace(custom_segment) + + +def test_trace_local_coordinate_system(): + radial_segment = geo.RadialHorizontalTraceSegment(1, np.pi) + linear_segment = geo.LinearHorizontalTraceSegment(1) + + # check with default coordinate system ---------------- + trace = geo.Trace([radial_segment, linear_segment]) + + # check first segment + for i in range(11): + weight = i / 10 + position = radial_segment.length * weight + cs_trace = trace.local_coordinate_system(position) + cs_segment = radial_segment.local_coordinate_system(weight) + + helpers.check_matrices_identical(cs_trace.basis, cs_segment.basis) + helpers.check_vectors_identical(cs_trace.origin, cs_segment.origin) + + # check second segment + expected_basis = radial_segment.local_coordinate_system(1).basis + for i in range(11): + weight = i / 10 + position_on_segment = linear_segment.length * weight + position = radial_segment.length + position_on_segment + + expected_origin = np.array([-position_on_segment, 2, 0]) + cs_trace = trace.local_coordinate_system(position) + + helpers.check_matrices_identical(cs_trace.basis, expected_basis) + helpers.check_vectors_identical(cs_trace.origin, expected_origin) + + # check with arbitrary coordinate system -------------- + basis = tf.rotation_matrix_x(np.pi / 2) + origin = np.array([-3, 2.5, 5]) + cs_base = tf.CoordinateSystem(basis, origin) + + trace = geo.Trace([radial_segment, linear_segment], cs_base) + + # check first segment + for i in range(11): + weight = i / 10 + position = radial_segment.length * weight + cs_trace = trace.local_coordinate_system(position) + cs_segment = radial_segment.local_coordinate_system(weight) + + expected_basis = np.matmul(basis, cs_segment.basis) + expected_origin = np.matmul(basis, cs_segment.origin) + origin + + helpers.check_matrices_identical(cs_trace.basis, expected_basis) + helpers.check_vectors_identical(cs_trace.origin, expected_origin) + + # check second segment + expected_basis = np.matmul(basis, + radial_segment.local_coordinate_system(1).basis) + for i in range(11): + weight = i / 10 + position_on_segment = linear_segment.length * weight + position = radial_segment.length + position_on_segment + + expected_origin = np.array([-position_on_segment, 0, 2]) + origin + cs_trace = trace.local_coordinate_system(position) + + helpers.check_matrices_identical(cs_trace.basis, expected_basis) + helpers.check_vectors_identical(cs_trace.origin, expected_origin) + + +def test_trace_rasterization(): + radial_segment = geo.RadialHorizontalTraceSegment(1, np.pi) + linear_segment = geo.LinearHorizontalTraceSegment(1) + + # check with default coordinate system ---------------- + trace = geo.Trace([linear_segment, radial_segment]) + data = trace.rasterize(0.1) + + # no duplications + assert helpers.are_all_points_unique(data) + + raster_width_eff = trace.length / (data.shape[1] - 1) + for i in range(data.shape[1]): + trace_location = i * raster_width_eff + if trace_location <= 1: + helpers.check_vectors_identical([trace_location, 0, 0], data[:, i]) + else: + arc_location = trace_location - 1 + angle = arc_location # radius 1! + x = np.sin(angle) + 1 # radius 1! + y = 1 - np.cos(angle) + helpers.check_vectors_identical([x, y, 0], data[:, i]) + + # check with arbitrary coordinate system -------------- + basis = tf.rotation_matrix_y(np.pi / 2) + origin = np.array([-3, 2.5, 5]) + cs_base = tf.CoordinateSystem(basis, origin) + + trace = geo.Trace([linear_segment, radial_segment], cs_base) + data = trace.rasterize(0.1) + + raster_width_eff = trace.length / (data.shape[1] - 1) + + for i in range(data.shape[1]): + trace_location = i * raster_width_eff + if trace_location <= 1: + x = origin[0] + y = origin[1] + z = origin[2] - trace_location + else: + arc_location = trace_location - 1 + angle = arc_location # radius 1! + x = origin[0] + y = origin[1] + 1 - np.cos(angle) + z = origin[2] - 1 - np.sin(angle) + + helpers.check_vectors_identical([x, y, z], data[:, i]) + + # check if raster width is clipped to valid range ----- + data = trace.rasterize(1000) + + assert data.shape[1] == 2 + helpers.check_vectors_identical([-3, 2.5, 5], data[:, 0]) + helpers.check_vectors_identical([-3, 4.5, 4], data[:, 1]) + + # exceptions ------------------------------------------ + with pytest.raises(Exception): + trace.rasterize(0) + with pytest.raises(Exception): + trace.rasterize(-23.1) + + +# Profile interpolation classes ----------------------------------------------- + + +def check_interpolated_profile_points(profile, c_0, c_1, c_2): + helpers.check_vectors_identical(profile.shapes[0].segments[0].point_start, + c_0) + helpers.check_vectors_identical(profile.shapes[0].segments[0].point_end, + c_1) + helpers.check_vectors_identical(profile.shapes[1].segments[0].point_start, + c_1) + helpers.check_vectors_identical(profile.shapes[1].segments[0].point_end, + c_2) + + +def test_linear_profile_interpolation_sbs(): + a_0 = [0, 0] + a_1 = [8, 16] + a_2 = [16, 0] + shape_a01 = geo.Shape(geo.LineSegment.construct_with_points(a_0, a_1)) + shape_a12 = geo.Shape(geo.LineSegment.construct_with_points(a_1, a_2)) + profile_a = geo.Profile([shape_a01, shape_a12]) + + b_0 = [-4, 8] + b_1 = [0, 8] + b_2 = [16, -16] + shape_b01 = geo.Shape(geo.LineSegment.construct_with_points(b_0, b_1)) + shape_b12 = geo.Shape(geo.LineSegment.construct_with_points(b_1, b_2)) + profile_b = geo.Profile([shape_b01, shape_b12]) + + [profile_a, profile_b] = get_default_profiles() + + for i in range(5): + weight = i / 4. + profile_c = geo.LinearProfileInterpolationSBS.interpolate(profile_a, + profile_b, + weight) + check_interpolated_profile_points(profile_c, + [-i, 2 * i], + [8 - 2 * i, 16 - 2 * i], + [16, -4 * i]) + + # check weight clipped to valid range ----------------- + + profile_c = geo.LinearProfileInterpolationSBS.interpolate(profile_a, + profile_b, + -3) + + check_interpolated_profile_points(profile_c, a_0, a_1, a_2) + + profile_c = geo.LinearProfileInterpolationSBS.interpolate(profile_a, + profile_b, + 42) + + check_interpolated_profile_points(profile_c, b_0, b_1, b_2) + + # exceptions ------------------------------------------ + + # number of shapes differ + profile_d = geo.Profile([shape_b01, shape_b12, shape_a12]) + with pytest.raises(Exception): + geo.LinearProfileInterpolationSBS.interpolate(profile_d, profile_b, + 0.5) + + # number of segments differ + shape_b012 = geo.Shape([geo.LineSegment.construct_with_points(b_0, b_1), + geo.LineSegment.construct_with_points(b_1, b_2)]) + + profile_b2 = geo.Profile([shape_b01, shape_b012]) + with pytest.raises(Exception): + geo.LinearProfileInterpolationSBS.interpolate(profile_a, + profile_b2, + 0.2) + + +# test variable profile ------------------------------------------------------- + +def check_variable_profile_state(variable_profile, locations): + num_profiles = len(locations) + assert variable_profile.num_interpolation_schemes == num_profiles - 1 + assert variable_profile.num_locations == num_profiles + assert variable_profile.num_profiles == num_profiles + + for i in range(num_profiles): + assert math.isclose(locations[i], variable_profile.locations[i]) + + +def test_variable_profile_construction(): + interpol = geo.LinearProfileInterpolationSBS + + profile_a, profile_b = get_default_profiles() + + # construction with single location and interpolation + variable_profile = geo.VariableProfile([profile_a, profile_b], + 1, + interpol) + check_variable_profile_state(variable_profile, [0, 1]) + variable_profile = geo.VariableProfile([profile_a, profile_b], + [1], + [interpol]) + check_variable_profile_state(variable_profile, [0, 1]) + + # construction with location list + variable_profile = geo.VariableProfile([profile_a, profile_b], + [0, 1], + interpol) + check_variable_profile_state(variable_profile, [0, 1]) + + variable_profile = geo.VariableProfile([profile_a, profile_b, profile_a], + [1, 2], + [interpol, interpol]) + check_variable_profile_state(variable_profile, [0, 1, 2]) + + variable_profile = geo.VariableProfile([profile_a, profile_b, profile_a], + [0, 1, 2], + [interpol, interpol]) + check_variable_profile_state(variable_profile, [0, 1, 2]) + + # exceptions ------------------------------------------ + + # first location is not 0 + with pytest.raises(Exception): + geo.VariableProfile([profile_a, profile_b], [1, 2], interpol) + + # number of locations is not correct + with pytest.raises(Exception): + geo.VariableProfile([profile_a, profile_b, profile_a], [1], + [interpol, interpol]) + with pytest.raises(Exception): + geo.VariableProfile([profile_a, profile_b], [0, 1, 2], + interpol) + + # number of interpolations is not correct + with pytest.raises(Exception): + geo.VariableProfile([profile_a, profile_b, profile_a], [0, 1, 2], + [interpol]) + with pytest.raises(Exception): + geo.VariableProfile([profile_a, profile_b, profile_a], [0, 1, 2], + [interpol, interpol, interpol]) + + # locations not ordered + with pytest.raises(Exception): + geo.VariableProfile([profile_a, profile_b, profile_a], [0, 2, 1], + [interpol, interpol]) + + +def test_variable_profile_local_profile(): + interpol = geo.LinearProfileInterpolationSBS + + profile_a, profile_b = get_default_profiles() + variable_profile = geo.VariableProfile([profile_a, profile_b, profile_a], + [0, 1, 2], + [interpol, interpol]) + + for i in range(5): + # first segment + location = i / 4. + profile = variable_profile.local_profile(location) + check_interpolated_profile_points(profile, + [-i, 2 * i], + [8 - 2 * i, 16 - 2 * i], + [16, -4 * i]) + # second segment + location += 1 + profile = variable_profile.local_profile(location) + check_interpolated_profile_points(profile, + [-4 + i, 8 - 2 * i], + [2 * i, 8 + 2 * i], + [16, -16 + 4 * i]) + + # check if values are clipped to valid range + + profile = variable_profile.local_profile(177) + check_interpolated_profile_points(profile, [0, 0], [8, 16], [16, 0]) + + profile = variable_profile.local_profile(-2) + check_interpolated_profile_points(profile, [0, 0], [8, 16], [16, 0]) + + +# test geometry class --------------------------------------------------------- + +def check_variable_profiles_identical(a, b): + assert a.num_profiles == b.num_profiles + assert a.num_locations == b.num_locations + assert a.num_interpolation_schemes == b.num_interpolation_schemes + + for i in range(a.num_profiles): + check_profiles_identical(a.profiles[i], b.profiles[i]) + for i in range(a.num_locations): + assert math.isclose(a.locations[i], b.locations[i]) + for i in range(a.num_interpolation_schemes): + assert isinstance(a.interpolation_schemes[i], + type(b.interpolation_schemes[i])) + + +def test_geometry_construction(): + profile_a, profile_b = get_default_profiles() + variable_profile = geo.VariableProfile([profile_a, profile_b], [0, 1], + geo.LinearProfileInterpolationSBS) + + radial_segment = geo.RadialHorizontalTraceSegment(1, np.pi) + linear_segment = geo.LinearHorizontalTraceSegment(1) + trace = geo.Trace([radial_segment, linear_segment]) + + # single profile construction + geometry = geo.Geometry(profile_a, trace) + check_profiles_identical(geometry.profile, profile_a) + check_traces_identical(geometry.trace, trace) + + # variable profile construction + geometry = geo.Geometry(variable_profile, trace) + check_variable_profiles_identical(geometry.profile, variable_profile) + check_traces_identical(geometry.trace, trace) + + # exceptions ------------------------------------------ + + # wrong types + with pytest.raises(TypeError): + geo.Geometry(variable_profile, profile_b) + with pytest.raises(TypeError): + geo.Geometry(trace, trace) + with pytest.raises(TypeError): + geo.Geometry(trace, profile_b) + with pytest.raises(TypeError): + geo.Geometry(variable_profile, "a") + with pytest.raises(TypeError): + geo.Geometry("42", trace) + + +def test_geometry_rasterization_trace(): + a0 = [1, 0] + a1 = [1, 1] + a2 = [0, 1] + a3 = [-1, 1] + a4 = [-1, 0] + + shape_a012 = geo.Shape([geo.LineSegment.construct_with_points(a0, a1), + geo.LineSegment.construct_with_points(a1, a2)]) + shape_a234 = geo.Shape([geo.LineSegment.construct_with_points(a2, a3), + geo.LineSegment.construct_with_points(a3, a4)]) + + profile_a = geo.Profile([shape_a012, shape_a234]) + + radial_segment = geo.RadialHorizontalTraceSegment(1, np.pi / 2, False) + linear_segment = geo.LinearHorizontalTraceSegment(1) + trace = geo.Trace([linear_segment, radial_segment]) + + geometry = geo.Geometry(profile_a, trace) + + # Note, if the raster width is larger than the segment, it is automatically + # adjusted to the segment width. Hence each rasterized profile has 6 + # points, which were defined at the beginning of the test (a2 is + # included twice) + data = geometry.rasterize(7, 0.1) + + num_raster_profiles = int(np.round(data.shape[1] / 6)) + profile_points = np.array([a0, a1, a2, a2, a3, a4]).transpose() + + eff_raster_width = trace.length / (data.shape[1] / 6 - 1) + arc_point_distance_on_trace = 2 * np.sin(eff_raster_width / 2) + + for i in range(num_raster_profiles): + idx_0 = i * 6 + if data[0, idx_0 + 2] <= 1: + x = data[0, idx_0] + assert math.isclose(x, eff_raster_width * i, abs_tol=1E-6) + for j in range(6): + assert math.isclose(data[1, idx_0 + j], profile_points[0, j]) + assert math.isclose(data[2, idx_0 + j], profile_points[1, j]) + assert math.isclose(data[0, idx_0 + j], data[0, idx_0]) + else: + assert math.isclose(data[0, idx_0], 1) + assert math.isclose(data[1, idx_0], a0[0]) + assert math.isclose(data[2, idx_0], a0[1]) + assert math.isclose(data[0, idx_0 + 1], 1) + assert math.isclose(data[1, idx_0 + 1], a1[0]) + assert math.isclose(data[2, idx_0 + 1], a1[1]) + + # z-values are constant + for j in np.arange(2, 6, 1): + assert math.isclose(data[2, idx_0 + j], profile_points[1, j]) + + # all profile points in a common x-y plane + exp_radius = np.array([1, 1, 2, 2]) + vec_02 = data[0:2, idx_0 + 2] - data[0:2, idx_0] + assert math.isclose(np.linalg.norm(vec_02), exp_radius[0]) + for j in np.arange(3, 6, 1): + vec_0j = data[0:2, idx_0 + j] - data[0:2, idx_0] + assert math.isclose(np.linalg.norm(vec_0j), exp_radius[j - 2]) + unit_vec_0j = tf.normalize(vec_0j) + assert math.isclose(np.dot(unit_vec_0j, vec_02), 1) + + # check point distance between profiles + if data[1, idx_0 - 4] > 1: + exp_point_distance = arc_point_distance_on_trace * exp_radius + for j in np.arange(2, 6, 1): + point_distance = np.linalg.norm( + data[:, idx_0 + j] - data[:, idx_0 + j - 6]) + assert math.isclose(exp_point_distance[j - 2], + point_distance) + + # check if raster width is clipped to valid range ----- + data = geometry.rasterize(7, 1000) + + assert data.shape[1] == 12 + + for i in range(12): + if i < 6: + math.isclose(data[0, i], 0) + else: + assert math.isclose(data[1, i], 1) + + # exceptions ------------------------------------------ + with pytest.raises(Exception): + geometry.rasterize(0, 1) + with pytest.raises(Exception): + geometry.rasterize(1, 0) + with pytest.raises(Exception): + geometry.rasterize(0, 0) + with pytest.raises(Exception): + geometry.rasterize(-2.3, 1) + with pytest.raises(Exception): + geometry.rasterize(1, -4.6) + with pytest.raises(Exception): + geometry.rasterize(-2.3, -4.6) + + +def test_geometry_rasterization_profile_interpolation(): + interpol = geo.LinearProfileInterpolationSBS + + a0 = [1, 0] + a1 = [1, 1] + a2 = [0, 1] + a3 = [-1, 1] + a4 = [-1, 0] + + shape_a012 = geo.Shape([geo.LineSegment.construct_with_points(a0, a1), + geo.LineSegment.construct_with_points(a1, a2)]) + shape_a234 = geo.Shape([geo.LineSegment.construct_with_points(a2, a3), + geo.LineSegment.construct_with_points(a3, a4)]) + + shape_b012 = copy.deepcopy(shape_a012) + shape_b234 = copy.deepcopy(shape_a234) + shape_b012.apply_transformation([[2, 0], [0, 2]]) + shape_b234.apply_transformation([[2, 0], [0, 2]]) + + profile_a = geo.Profile([shape_a012, shape_a234]) + profile_b = geo.Profile([shape_b012, shape_b234]) + + variable_profile = geo.VariableProfile([profile_a, profile_b, profile_a], + [0, 2, 6], [interpol, interpol]) + + linear_segment_l1 = geo.LinearHorizontalTraceSegment(1) + linear_segment_l2 = geo.LinearHorizontalTraceSegment(2) + # Note: The profile in the middle is not located at the start of the + # second segment + trace = geo.Trace([linear_segment_l2, linear_segment_l1]) + + geometry = geo.Geometry(variable_profile, trace) + + # Note: If the raster width is larger than the segment, it is automatically + # adjusted to the segment width. Hence each rasterized profile has 6 + # points, which were defined at the beginning of the test (a2 is + # included twice) + data = geometry.rasterize(7, 0.1) + assert data.shape[1] == 186 + + profile_points = np.array([a0, a1, a2, a2, a3, a4]).transpose() + + # check first segment + for i in range(11): + idx_0 = i * 6 + for j in range(6): + exp_point = np.array([i * 0.1, + profile_points[0, j] * (1 + i * 0.1), + profile_points[1, j] * (1 + i * 0.1)]) + helpers.check_vectors_identical(data[:, idx_0 + j], exp_point) + + # check second segment + for i in range(20): + idx_0 = (30 - i) * 6 + for j in range(6): + exp_point = np.array([3 - i * 0.1, + profile_points[0, j] * (1 + i * 0.05), + profile_points[1, j] * (1 + i * 0.05)]) + helpers.check_vectors_identical(data[:, idx_0 + j], exp_point) diff --git a/tests/test_point_cloud_generator.py b/tests/test_point_cloud_generator.py deleted file mode 100644 index cfb1b5a..0000000 --- a/tests/test_point_cloud_generator.py +++ /dev/null @@ -1,870 +0,0 @@ -import pytest -import mypackage.point_cloud_generator as pcg -import mypackage.geometry as geo -import mypackage.transformations as tf -import math -import numpy as np -import copy -import tests._helpers as helpers - - -# helpers --------------------------------------------------------------------- - -def check_profiles_identical(a, b): - assert a.num_shapes == b.num_shapes - for i in range(a.num_shapes): - check_shapes_identical(a.shapes[i], b.shapes[i]) - - -def check_segments_identical(a, b): - assert isinstance(a, type(b)) - helpers.check_vectors_identical(a.point_start, b.point_start) - helpers.check_vectors_identical(a.point_end, b.point_end) - if isinstance(a, geo.ArcSegment): - assert a.arc_winding_ccw == b.arc_winding_ccw - helpers.check_vectors_identical(a.point_center, b.point_center) - - -def check_shapes_identical(a, b): - assert a.num_segments == b.num_segments - for i in range(a.num_segments): - check_segments_identical(a.segments[i], b.segments[i]) - - -def check_trace_segments_identical(a, b): - assert isinstance(a, type(b)) - if isinstance(a, pcg.LinearHorizontalTraceSegment): - assert a.length == b.length - else: - assert a.is_clockwise == b.is_clockwise - assert math.isclose(a.angle, b.angle) - assert math.isclose(a.length, b.length) - assert math.isclose(a.radius, b.radius) - - -def check_traces_identical(a, b): - assert a.num_segments == b.num_segments - for i in range(a.num_segments): - check_trace_segments_identical(a.segments[i], b.segments[i]) - - -def get_default_profiles(): - a_0 = [0, 0] - a_1 = [8, 16] - a_2 = [16, 0] - shape_a01 = geo.Shape(geo.LineSegment.construct_with_points(a_0, a_1)) - shape_a12 = geo.Shape(geo.LineSegment.construct_with_points(a_1, a_2)) - profile_a = pcg.Profile([shape_a01, shape_a12]) - - b_0 = [-4, 8] - b_1 = [0, 8] - b_2 = [16, -16] - shape_b01 = geo.Shape(geo.LineSegment.construct_with_points(b_0, b_1)) - shape_b12 = geo.Shape(geo.LineSegment.construct_with_points(b_1, b_2)) - profile_b = pcg.Profile([shape_b01, shape_b12]) - return [profile_a, profile_b] - - -# Test profile class ---------------------------------------------------------- - -def test_profile_construction_and_shape_addition(): - segment0 = geo.LineSegment.construct_with_points([0, 0], [1, 0]) - segment1 = geo.LineSegment.construct_with_points([1, 0], [2, -1]) - segment2 = geo.LineSegment.construct_with_points([2, -1], [0, -1]) - - shape = geo.Shape([segment0, segment1, segment2]) - - # Check invalid types - with pytest.raises(TypeError): - pcg.Profile(3) - with pytest.raises(TypeError): - pcg.Profile("This is not right") - with pytest.raises(TypeError): - pcg.Profile([2, 8, 1]) - - # Check valid types - profile = pcg.Profile(shape) - assert profile.num_shapes == 1 - profile = pcg.Profile([shape, shape]) - assert profile.num_shapes == 2 - - # Check invalid addition - with pytest.raises(TypeError): - profile.add_shapes([shape, 0.1]) - with pytest.raises(TypeError): - profile.add_shapes(["shape"]) - with pytest.raises(TypeError): - profile.add_shapes(0.1) - - # Check that invalid calls only raise an exception and do not invalidate - # the internal data - assert profile.num_shapes == 2 - - # Check valid addition - profile.add_shapes(shape) - assert profile.num_shapes == 3 - profile.add_shapes([shape, shape]) - assert profile.num_shapes == 5 - - # Check shapes - shapes_profile = profile.shapes - for shape_profile in shapes_profile: - assert shape.num_segments == shape_profile.num_segments - - segments = shape.segments - segments_profile = shape_profile.segments - - assert len(segments) == shape.num_segments - assert len(segments) == len(segments_profile) - - for i in range(shape.num_segments): - assert isinstance(segments_profile[i], type(segments[i])) - points = segments[i].points - points_profile = segments_profile[i].points - for j in range(2): - helpers.check_vectors_identical(points[:, j], - points_profile[:, j]) - - -def test_profile_rasterization(): - raster_width = 0.1 - shape0 = geo.Shape( - geo.LineSegment.construct_with_points([-1, 0], [-raster_width, 0])) - shape1 = geo.Shape(geo.LineSegment.construct_with_points([0, 0], [1, 0])) - shape2 = geo.Shape( - geo.LineSegment.construct_with_points([1 + raster_width, 0], [2, 0])) - - profile = pcg.Profile([shape0, shape1]) - profile.add_shapes(shape2) - - # rasterize - data = profile.rasterize(0.1) - - # no duplications - assert helpers.are_all_points_unique(data) - - # check raster data size - expected_number_raster_points = int(round(3 / raster_width)) + 1 - assert data.shape[1] == expected_number_raster_points - - # Check that all shapes are rasterized correct - for i in range(int(round(3 / raster_width)) + 1): - expected_raster_point_x = i * raster_width - 1 - assert data[0, i] - expected_raster_point_x < 1E-9 - assert data[1, i] == 0 - - # exceptions - with pytest.raises(Exception): - profile.rasterize(0) - with pytest.raises(Exception): - profile.rasterize(-3) - - -# Test trace segment classes -------------------------------------------------- - -def check_trace_segment_length(segment, tolerance=1E-9): - lcs = segment.local_coordinate_system(1) - length_numeric_prev = np.linalg.norm(lcs.origin) - - # calculate numerical length by linearization - num_segments = 2. - num_iterations = 20 - - # calculate numerical length with increasing number of segments until - # the rate of change between 2 calculations is small enough - for i in range(num_iterations): - length_numeric = 0 - increment = 1. / num_segments - - ccs0 = segment.local_coordinate_system(0) - for rel_pos in (np.arange(increment, 1. + increment / 2, increment)): - ccs1 = segment.local_coordinate_system(rel_pos) - length_numeric += np.linalg.norm(ccs1.origin - ccs0.origin) - ccs0 = copy.deepcopy(ccs1) - - relative_change = length_numeric / length_numeric_prev - - length_numeric_prev = copy.deepcopy(length_numeric) - num_segments *= 2 - - if math.isclose(relative_change, 1, abs_tol=tolerance / 10): - break - assert i < num_iterations - 1, "Segment length could not be " \ - "determined numerically" - - assert math.isclose(length_numeric, segment.length, abs_tol=tolerance) - - -def check_trace_segment_orientation(segment): - # The initial orientation of a segment must be [0, 1, 0] - lcs = segment.local_coordinate_system(0) - helpers.check_vectors_identical(lcs.basis[:, 1], np.array([0, 1, 0])) - - delta = 1E-9 - for rel_pos in np.arange(0.1, 1.01, 0.1): - lcs = segment.local_coordinate_system(rel_pos) - lcs_d = segment.local_coordinate_system(rel_pos - delta) - trace_direction_numerical = tf.normalize(lcs.origin - lcs_d.origin) - - # Check that the y-axis is always aligned with the trace's direction - helpers.check_vectors_identical(lcs.basis[:, 0], - trace_direction_numerical, 1E-6) - - -def default_trace_segment_tests(segment, tolerance_length=1E-9): - lcs = segment.local_coordinate_system(0) - - # test that function actually returns a coordinate system class - assert isinstance(lcs, tf.CoordinateSystem) - - # check that origin for weight 0 is at [0,0,0] - for i in range(3): - assert math.isclose(lcs.origin[i], 0) - - check_trace_segment_length(segment, tolerance_length) - check_trace_segment_orientation(segment) - - -def test_linear_horizontal_trace_segment(): - length = 7.13 - segment = pcg.LinearHorizontalTraceSegment(length) - - # default tests - default_trace_segment_tests(segment) - - # getter tests - assert math.isclose(segment.length, length) - - # invalid inputs - with pytest.raises(ValueError): - pcg.LinearHorizontalTraceSegment(0) - with pytest.raises(ValueError): - pcg.LinearHorizontalTraceSegment(-4.61) - - -def test_radial_horizontal_trace_segment(): - radius = 4.74 - angle = np.pi / 1.23 - segment_cw = pcg.RadialHorizontalTraceSegment(radius, angle, True) - segment_ccw = pcg.RadialHorizontalTraceSegment(radius, angle, False) - - # default tests - default_trace_segment_tests(segment_cw, 1E-4) - default_trace_segment_tests(segment_ccw, 1E-4) - - # getter tests - assert math.isclose(segment_cw.angle, angle) - assert math.isclose(segment_ccw.angle, angle) - assert math.isclose(segment_cw.radius, radius) - assert math.isclose(segment_ccw.radius, radius) - assert segment_cw.is_clockwise - assert not segment_ccw.is_clockwise - - # check positions - for weight in np.arange(0.1, 1, 0.1): - current_angle = angle * weight - x_exp = np.sin(current_angle) * radius - y_exp = (1 - np.cos(current_angle)) * radius - - lcs_cw = segment_cw.local_coordinate_system(weight) - lcs_ccw = segment_ccw.local_coordinate_system(weight) - - assert math.isclose(lcs_cw.origin[0], x_exp) - assert math.isclose(lcs_cw.origin[1], -y_exp) - assert math.isclose(lcs_ccw.origin[0], x_exp) - assert math.isclose(lcs_ccw.origin[1], y_exp) - - # invalid inputs - with pytest.raises(ValueError): - pcg.RadialHorizontalTraceSegment(0, np.pi) - with pytest.raises(ValueError): - pcg.RadialHorizontalTraceSegment(-0.53, np.pi) - with pytest.raises(ValueError): - pcg.RadialHorizontalTraceSegment(1, 0) - with pytest.raises(ValueError): - pcg.RadialHorizontalTraceSegment(1, -np.pi) - - -# Test trace class ------------------------------------------------------------ - -def test_trace_construction(): - linear_segment = pcg.LinearHorizontalTraceSegment(1) - radial_segment = pcg.RadialHorizontalTraceSegment(1, np.pi) - ccs_origin = np.array([2, 3, -2]) - ccs = helpers.rotated_coordinate_system(origin=ccs_origin) - - # test single segment construction -------------------- - trace = pcg.Trace(linear_segment, ccs) - assert math.isclose(trace.length, linear_segment.length) - assert trace.num_segments == 1 - - segments = trace.segments - assert len(segments) == 1 - assert isinstance(segments[0], type(linear_segment)) - assert math.isclose(linear_segment.length, segments[0].length) - - helpers.check_matrices_identical(ccs.basis, trace.coordinate_system.basis) - helpers.check_vectors_identical(ccs.origin, trace.coordinate_system.origin) - - # test multi segment construction --------------------- - trace = pcg.Trace([radial_segment, linear_segment]) - assert math.isclose(trace.length, - linear_segment.length + radial_segment.length) - assert trace.num_segments == 2 - - segments = trace.segments - assert len(segments) == 2 - assert isinstance(segments[0], type(radial_segment)) - assert isinstance(segments[1], type(linear_segment)) - - assert math.isclose(radial_segment.radius, segments[0].radius) - assert math.isclose(radial_segment.angle, segments[0].angle) - assert math.isclose(radial_segment.is_clockwise, segments[0].is_clockwise) - assert math.isclose(linear_segment.length, segments[1].length) - - helpers.check_matrices_identical(np.identity(3), - trace.coordinate_system.basis) - helpers.check_vectors_identical(np.array([0, 0, 0]), - trace.coordinate_system.origin) - - # check invalid inputs -------------------------------- - with pytest.raises(TypeError): - pcg.Trace(radial_segment, linear_segment) - with pytest.raises(TypeError): - pcg.Trace(radial_segment, 2) - with pytest.raises(Exception): - pcg.Trace(None) - - # check construction with custom segment -------------- - class CustomSegment(): - def __init__(self): - self.length = None - - @staticmethod - def local_coordinate_system(*args): - return tf.CoordinateSystem() - - custom_segment = CustomSegment() - custom_segment.length = 3 - pcg.Trace(custom_segment) - - with pytest.raises(Exception): - custom_segment.length = -12 - pcg.Trace(custom_segment) - with pytest.raises(Exception): - custom_segment.length = 0 - pcg.Trace(custom_segment) - - -def test_trace_local_coordinate_system(): - radial_segment = pcg.RadialHorizontalTraceSegment(1, np.pi) - linear_segment = pcg.LinearHorizontalTraceSegment(1) - - # check with default coordinate system ---------------- - trace = pcg.Trace([radial_segment, linear_segment]) - - # check first segment - for i in range(11): - weight = i / 10 - position = radial_segment.length * weight - cs_trace = trace.local_coordinate_system(position) - cs_segment = radial_segment.local_coordinate_system(weight) - - helpers.check_matrices_identical(cs_trace.basis, cs_segment.basis) - helpers.check_vectors_identical(cs_trace.origin, cs_segment.origin) - - # check second segment - expected_basis = radial_segment.local_coordinate_system(1).basis - for i in range(11): - weight = i / 10 - position_on_segment = linear_segment.length * weight - position = radial_segment.length + position_on_segment - - expected_origin = np.array([-position_on_segment, 2, 0]) - cs_trace = trace.local_coordinate_system(position) - - helpers.check_matrices_identical(cs_trace.basis, expected_basis) - helpers.check_vectors_identical(cs_trace.origin, expected_origin) - - # check with arbitrary coordinate system -------------- - basis = tf.rotation_matrix_x(np.pi / 2) - origin = np.array([-3, 2.5, 5]) - cs_base = tf.CoordinateSystem(basis, origin) - - trace = pcg.Trace([radial_segment, linear_segment], cs_base) - - # check first segment - for i in range(11): - weight = i / 10 - position = radial_segment.length * weight - cs_trace = trace.local_coordinate_system(position) - cs_segment = radial_segment.local_coordinate_system(weight) - - expected_basis = np.matmul(basis, cs_segment.basis) - expected_origin = np.matmul(basis, cs_segment.origin) + origin - - helpers.check_matrices_identical(cs_trace.basis, expected_basis) - helpers.check_vectors_identical(cs_trace.origin, expected_origin) - - # check second segment - expected_basis = np.matmul(basis, - radial_segment.local_coordinate_system(1).basis) - for i in range(11): - weight = i / 10 - position_on_segment = linear_segment.length * weight - position = radial_segment.length + position_on_segment - - expected_origin = np.array([-position_on_segment, 0, 2]) + origin - cs_trace = trace.local_coordinate_system(position) - - helpers.check_matrices_identical(cs_trace.basis, expected_basis) - helpers.check_vectors_identical(cs_trace.origin, expected_origin) - - -def test_trace_rasterization(): - radial_segment = pcg.RadialHorizontalTraceSegment(1, np.pi) - linear_segment = pcg.LinearHorizontalTraceSegment(1) - - # check with default coordinate system ---------------- - trace = pcg.Trace([linear_segment, radial_segment]) - data = trace.rasterize(0.1) - - # no duplications - assert helpers.are_all_points_unique(data) - - raster_width_eff = trace.length / (data.shape[1] - 1) - for i in range(data.shape[1]): - trace_location = i * raster_width_eff - if trace_location <= 1: - helpers.check_vectors_identical([trace_location, 0, 0], data[:, i]) - else: - arc_location = trace_location - 1 - angle = arc_location # radius 1! - x = np.sin(angle) + 1 # radius 1! - y = 1 - np.cos(angle) - helpers.check_vectors_identical([x, y, 0], data[:, i]) - - # check with arbitrary coordinate system -------------- - basis = tf.rotation_matrix_y(np.pi / 2) - origin = np.array([-3, 2.5, 5]) - cs_base = tf.CoordinateSystem(basis, origin) - - trace = pcg.Trace([linear_segment, radial_segment], cs_base) - data = trace.rasterize(0.1) - - raster_width_eff = trace.length / (data.shape[1] - 1) - - for i in range(data.shape[1]): - trace_location = i * raster_width_eff - if trace_location <= 1: - x = origin[0] - y = origin[1] - z = origin[2] - trace_location - else: - arc_location = trace_location - 1 - angle = arc_location # radius 1! - x = origin[0] - y = origin[1] + 1 - np.cos(angle) - z = origin[2] - 1 - np.sin(angle) - - helpers.check_vectors_identical([x, y, z], data[:, i]) - - # check if raster width is clipped to valid range ----- - data = trace.rasterize(1000) - - assert data.shape[1] == 2 - helpers.check_vectors_identical([-3, 2.5, 5], data[:, 0]) - helpers.check_vectors_identical([-3, 4.5, 4], data[:, 1]) - - # exceptions ------------------------------------------ - with pytest.raises(Exception): - trace.rasterize(0) - with pytest.raises(Exception): - trace.rasterize(-23.1) - - -# Profile interpolation classes ----------------------------------------------- - - -def check_interpolated_profile_points(profile, c_0, c_1, c_2): - helpers.check_vectors_identical(profile.shapes[0].segments[0].point_start, - c_0) - helpers.check_vectors_identical(profile.shapes[0].segments[0].point_end, - c_1) - helpers.check_vectors_identical(profile.shapes[1].segments[0].point_start, - c_1) - helpers.check_vectors_identical(profile.shapes[1].segments[0].point_end, - c_2) - - -def test_linear_profile_interpolation_sbs(): - a_0 = [0, 0] - a_1 = [8, 16] - a_2 = [16, 0] - shape_a01 = geo.Shape(geo.LineSegment.construct_with_points(a_0, a_1)) - shape_a12 = geo.Shape(geo.LineSegment.construct_with_points(a_1, a_2)) - profile_a = pcg.Profile([shape_a01, shape_a12]) - - b_0 = [-4, 8] - b_1 = [0, 8] - b_2 = [16, -16] - shape_b01 = geo.Shape(geo.LineSegment.construct_with_points(b_0, b_1)) - shape_b12 = geo.Shape(geo.LineSegment.construct_with_points(b_1, b_2)) - profile_b = pcg.Profile([shape_b01, shape_b12]) - - [profile_a, profile_b] = get_default_profiles() - - for i in range(5): - weight = i / 4. - profile_c = pcg.LinearProfileInterpolationSBS.interpolate(profile_a, - profile_b, - weight) - check_interpolated_profile_points(profile_c, - [-i, 2 * i], - [8 - 2 * i, 16 - 2 * i], - [16, -4 * i]) - - # check weight clipped to valid range ----------------- - - profile_c = pcg.LinearProfileInterpolationSBS.interpolate(profile_a, - profile_b, - -3) - - check_interpolated_profile_points(profile_c, a_0, a_1, a_2) - - profile_c = pcg.LinearProfileInterpolationSBS.interpolate(profile_a, - profile_b, - 42) - - check_interpolated_profile_points(profile_c, b_0, b_1, b_2) - - # exceptions ------------------------------------------ - - # number of shapes differ - profile_d = pcg.Profile([shape_b01, shape_b12, shape_a12]) - with pytest.raises(Exception): - pcg.LinearProfileInterpolationSBS.interpolate(profile_d, profile_b, - 0.5) - - # number of segments differ - shape_b012 = geo.Shape([geo.LineSegment.construct_with_points(b_0, b_1), - geo.LineSegment.construct_with_points(b_1, b_2)]) - - profile_b2 = pcg.Profile([shape_b01, shape_b012]) - with pytest.raises(Exception): - pcg.LinearProfileInterpolationSBS.interpolate(profile_a, - profile_b2, - 0.2) - - -# test variable profile ------------------------------------------------------- - -def check_variable_profile_state(variable_profile, locations): - num_profiles = len(locations) - assert variable_profile.num_interpolation_schemes == num_profiles - 1 - assert variable_profile.num_locations == num_profiles - assert variable_profile.num_profiles == num_profiles - - for i in range(num_profiles): - assert math.isclose(locations[i], variable_profile.locations[i]) - - -def test_variable_profile_construction(): - interpol = pcg.LinearProfileInterpolationSBS - - profile_a, profile_b = get_default_profiles() - - # construction with single location and interpolation - variable_profile = pcg.VariableProfile([profile_a, profile_b], - 1, - interpol) - check_variable_profile_state(variable_profile, [0, 1]) - variable_profile = pcg.VariableProfile([profile_a, profile_b], - [1], - [interpol]) - check_variable_profile_state(variable_profile, [0, 1]) - - # construction with location list - variable_profile = pcg.VariableProfile([profile_a, profile_b], - [0, 1], - interpol) - check_variable_profile_state(variable_profile, [0, 1]) - - variable_profile = pcg.VariableProfile([profile_a, profile_b, profile_a], - [1, 2], - [interpol, interpol]) - check_variable_profile_state(variable_profile, [0, 1, 2]) - - variable_profile = pcg.VariableProfile([profile_a, profile_b, profile_a], - [0, 1, 2], - [interpol, interpol]) - check_variable_profile_state(variable_profile, [0, 1, 2]) - - # exceptions ------------------------------------------ - - # first location is not 0 - with pytest.raises(Exception): - pcg.VariableProfile([profile_a, profile_b], [1, 2], interpol) - - # number of locations is not correct - with pytest.raises(Exception): - pcg.VariableProfile([profile_a, profile_b, profile_a], [1], - [interpol, interpol]) - with pytest.raises(Exception): - pcg.VariableProfile([profile_a, profile_b], [0, 1, 2], - interpol) - - # number of interpolations is not correct - with pytest.raises(Exception): - pcg.VariableProfile([profile_a, profile_b, profile_a], [0, 1, 2], - [interpol]) - with pytest.raises(Exception): - pcg.VariableProfile([profile_a, profile_b, profile_a], [0, 1, 2], - [interpol, interpol, interpol]) - - # locations not ordered - with pytest.raises(Exception): - pcg.VariableProfile([profile_a, profile_b, profile_a], [0, 2, 1], - [interpol, interpol]) - - -def test_variable_profile_local_profile(): - interpol = pcg.LinearProfileInterpolationSBS - - profile_a, profile_b = get_default_profiles() - variable_profile = pcg.VariableProfile([profile_a, profile_b, profile_a], - [0, 1, 2], - [interpol, interpol]) - - for i in range(5): - # first segment - location = i / 4. - profile = variable_profile.local_profile(location) - check_interpolated_profile_points(profile, - [-i, 2 * i], - [8 - 2 * i, 16 - 2 * i], - [16, -4 * i]) - # second segment - location += 1 - profile = variable_profile.local_profile(location) - check_interpolated_profile_points(profile, - [-4 + i, 8 - 2 * i], - [2 * i, 8 + 2 * i], - [16, -16 + 4 * i]) - - # check if values are clipped to valid range - - profile = variable_profile.local_profile(177) - check_interpolated_profile_points(profile, [0, 0], [8, 16], [16, 0]) - - profile = variable_profile.local_profile(-2) - check_interpolated_profile_points(profile, [0, 0], [8, 16], [16, 0]) - - -# test geometry class --------------------------------------------------------- - -def check_variable_profiles_identical(a, b): - assert a.num_profiles == b.num_profiles - assert a.num_locations == b.num_locations - assert a.num_interpolation_schemes == b.num_interpolation_schemes - - for i in range(a.num_profiles): - check_profiles_identical(a.profiles[i], b.profiles[i]) - for i in range(a.num_locations): - assert math.isclose(a.locations[i], b.locations[i]) - for i in range(a.num_interpolation_schemes): - assert isinstance(a.interpolation_schemes[i], - type(b.interpolation_schemes[i])) - - -def test_geometry_construction(): - profile_a, profile_b = get_default_profiles() - variable_profile = pcg.VariableProfile([profile_a, profile_b], [0, 1], - pcg.LinearProfileInterpolationSBS) - - radial_segment = pcg.RadialHorizontalTraceSegment(1, np.pi) - linear_segment = pcg.LinearHorizontalTraceSegment(1) - trace = pcg.Trace([radial_segment, linear_segment]) - - # single profile construction - geometry = pcg.Geometry(profile_a, trace) - check_profiles_identical(geometry.profile, profile_a) - check_traces_identical(geometry.trace, trace) - - # variable profile construction - geometry = pcg.Geometry(variable_profile, trace) - check_variable_profiles_identical(geometry.profile, variable_profile) - check_traces_identical(geometry.trace, trace) - - # exceptions ------------------------------------------ - - # wrong types - with pytest.raises(TypeError): - pcg.Geometry(variable_profile, profile_b) - with pytest.raises(TypeError): - pcg.Geometry(trace, trace) - with pytest.raises(TypeError): - pcg.Geometry(trace, profile_b) - with pytest.raises(TypeError): - pcg.Geometry(variable_profile, "a") - with pytest.raises(TypeError): - pcg.Geometry("42", trace) - - -def test_geometry_rasterization_trace(): - a0 = [1, 0] - a1 = [1, 1] - a2 = [0, 1] - a3 = [-1, 1] - a4 = [-1, 0] - - shape_a012 = geo.Shape([geo.LineSegment.construct_with_points(a0, a1), - geo.LineSegment.construct_with_points(a1, a2)]) - shape_a234 = geo.Shape([geo.LineSegment.construct_with_points(a2, a3), - geo.LineSegment.construct_with_points(a3, a4)]) - - profile_a = pcg.Profile([shape_a012, shape_a234]) - - radial_segment = pcg.RadialHorizontalTraceSegment(1, np.pi / 2, False) - linear_segment = pcg.LinearHorizontalTraceSegment(1) - trace = pcg.Trace([linear_segment, radial_segment]) - - geometry = pcg.Geometry(profile_a, trace) - - # Note, if the raster width is larger than the segment, it is automatically - # adjusted to the segment width. Hence each rasterized profile has 6 - # points, which were defined at the beginning of the test (a2 is - # included twice) - data = geometry.rasterize(7, 0.1) - - num_raster_profiles = int(np.round(data.shape[1] / 6)) - profile_points = np.array([a0, a1, a2, a2, a3, a4]).transpose() - - eff_raster_width = trace.length / (data.shape[1] / 6 - 1) - arc_point_distance_on_trace = 2 * np.sin(eff_raster_width / 2) - - for i in range(num_raster_profiles): - idx_0 = i * 6 - if data[0, idx_0 + 2] <= 1: - x = data[0, idx_0] - assert math.isclose(x, eff_raster_width * i, abs_tol=1E-6) - for j in range(6): - assert math.isclose(data[1, idx_0 + j], profile_points[0, j]) - assert math.isclose(data[2, idx_0 + j], profile_points[1, j]) - assert math.isclose(data[0, idx_0 + j], data[0, idx_0]) - else: - assert math.isclose(data[0, idx_0], 1) - assert math.isclose(data[1, idx_0], a0[0]) - assert math.isclose(data[2, idx_0], a0[1]) - assert math.isclose(data[0, idx_0 + 1], 1) - assert math.isclose(data[1, idx_0 + 1], a1[0]) - assert math.isclose(data[2, idx_0 + 1], a1[1]) - - # z-values are constant - for j in np.arange(2, 6, 1): - assert math.isclose(data[2, idx_0 + j], profile_points[1, j]) - - # all profile points in a common x-y plane - exp_radius = np.array([1, 1, 2, 2]) - vec_02 = data[0:2, idx_0 + 2] - data[0:2, idx_0] - assert math.isclose(np.linalg.norm(vec_02), exp_radius[0]) - for j in np.arange(3, 6, 1): - vec_0j = data[0:2, idx_0 + j] - data[0:2, idx_0] - assert math.isclose(np.linalg.norm(vec_0j), exp_radius[j - 2]) - unit_vec_0j = tf.normalize(vec_0j) - assert math.isclose(np.dot(unit_vec_0j, vec_02), 1) - - # check point distance between profiles - if data[1, idx_0 - 4] > 1: - exp_point_distance = arc_point_distance_on_trace * exp_radius - for j in np.arange(2, 6, 1): - point_distance = np.linalg.norm( - data[:, idx_0 + j] - data[:, idx_0 + j - 6]) - assert math.isclose(exp_point_distance[j - 2], - point_distance) - - # check if raster width is clipped to valid range ----- - data = geometry.rasterize(7, 1000) - - assert data.shape[1] == 12 - - for i in range(12): - if i < 6: - math.isclose(data[0, i], 0) - else: - assert math.isclose(data[1, i], 1) - - # exceptions ------------------------------------------ - with pytest.raises(Exception): - geometry.rasterize(0, 1) - with pytest.raises(Exception): - geometry.rasterize(1, 0) - with pytest.raises(Exception): - geometry.rasterize(0, 0) - with pytest.raises(Exception): - geometry.rasterize(-2.3, 1) - with pytest.raises(Exception): - geometry.rasterize(1, -4.6) - with pytest.raises(Exception): - geometry.rasterize(-2.3, -4.6) - - -def test_geometry_rasterization_profile_interpolation(): - interpol = pcg.LinearProfileInterpolationSBS - - a0 = [1, 0] - a1 = [1, 1] - a2 = [0, 1] - a3 = [-1, 1] - a4 = [-1, 0] - - shape_a012 = geo.Shape([geo.LineSegment.construct_with_points(a0, a1), - geo.LineSegment.construct_with_points(a1, a2)]) - shape_a234 = geo.Shape([geo.LineSegment.construct_with_points(a2, a3), - geo.LineSegment.construct_with_points(a3, a4)]) - - shape_b012 = copy.deepcopy(shape_a012) - shape_b234 = copy.deepcopy(shape_a234) - shape_b012.apply_transformation([[2, 0], [0, 2]]) - shape_b234.apply_transformation([[2, 0], [0, 2]]) - - profile_a = pcg.Profile([shape_a012, shape_a234]) - profile_b = pcg.Profile([shape_b012, shape_b234]) - - variable_profile = pcg.VariableProfile([profile_a, profile_b, profile_a], - [0, 2, 6], [interpol, interpol]) - - linear_segment_l1 = pcg.LinearHorizontalTraceSegment(1) - linear_segment_l2 = pcg.LinearHorizontalTraceSegment(2) - # Note: The profile in the middle is not located at the start of the - # second segment - trace = pcg.Trace([linear_segment_l2, linear_segment_l1]) - - geometry = pcg.Geometry(variable_profile, trace) - - # Note: If the raster width is larger than the segment, it is automatically - # adjusted to the segment width. Hence each rasterized profile has 6 - # points, which were defined at the beginning of the test (a2 is - # included twice) - data = geometry.rasterize(7, 0.1) - assert data.shape[1] == 186 - - profile_points = np.array([a0, a1, a2, a2, a3, a4]).transpose() - - # check first segment - for i in range(11): - idx_0 = i * 6 - for j in range(6): - exp_point = np.array([i * 0.1, - profile_points[0, j] * (1 + i * 0.1), - profile_points[1, j] * (1 + i * 0.1)]) - helpers.check_vectors_identical(data[:, idx_0 + j], exp_point) - - # check second segment - for i in range(20): - idx_0 = (30 - i) * 6 - for j in range(6): - exp_point = np.array([3 - i * 0.1, - profile_points[0, j] * (1 + i * 0.05), - profile_points[1, j] * (1 + i * 0.05)]) - helpers.check_vectors_identical(data[:, idx_0 + j], exp_point) From 4adc1c63c4e3968020db7b38e9de8420485174f0 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Wed, 5 Feb 2020 10:07:47 +0100 Subject: [PATCH 142/177] Increase coverage --- tests/test_geometry.py | 7 +++++++ tests/test_trasformations.py | 2 ++ 2 files changed, 9 insertions(+) diff --git a/tests/test_geometry.py b/tests/test_geometry.py index 9fd9d4e..451f541 100644 --- a/tests/test_geometry.py +++ b/tests/test_geometry.py @@ -326,6 +326,13 @@ def test_arc_segment_construction(): assert math.isclose(segment_cw.arc_length, 3 / 2 * np.pi) assert math.isclose(segment_ccw.arc_length, 9 / 2 * np.pi) + helpers.check_vectors_identical([3, 3], segment_cw.points[:, 0]) + helpers.check_vectors_identical([3, 3], segment_ccw.points[:, 0]) + helpers.check_vectors_identical([6, 6], segment_cw.points[:, 1]) + helpers.check_vectors_identical([6, 6], segment_ccw.points[:, 1]) + helpers.check_vectors_identical([6, 3], segment_cw.points[:, 2]) + helpers.check_vectors_identical([6, 3], segment_ccw.points[:, 2]) + # check exceptions ------------------------------------ # radius differs diff --git a/tests/test_trasformations.py b/tests/test_trasformations.py index b6b0cb1..f3a637b 100644 --- a/tests/test_trasformations.py +++ b/tests/test_trasformations.py @@ -24,9 +24,11 @@ def check_coordinate_system(ccs, basis_expected, origin_expected, # check axis orientations match assert np.abs(np.dot(ccs.basis[:, i], unit_vec) - 1) < 1E-9 + assert np.abs(np.dot(ccs.orientation[:, i], unit_vec) - 1) < 1E-9 # check origin correct assert np.abs(origin_expected[i] - ccs.origin[i]) < 1E-9 + assert np.abs(origin_expected[i] - ccs.location[i]) < 1E-9 def check_matrix_does_not_reflect(matrix): From 88a9d0319a9ee41701cbc0e302c1962c527e9543 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Wed, 5 Feb 2020 10:18:35 +0100 Subject: [PATCH 143/177] Replace class with function --- mypackage/geometry.py | 45 ++++++++++++++++++++---------------------- tests/test_geometry.py | 20 ++++++------------- 2 files changed, 27 insertions(+), 38 deletions(-) diff --git a/mypackage/geometry.py b/mypackage/geometry.py index 03691ca..21984dc 100644 --- a/mypackage/geometry.py +++ b/mypackage/geometry.py @@ -977,31 +977,27 @@ def rasterize(self, raster_width): # Linear profile interpolation class ------------------------------------------ -class LinearProfileInterpolationSBS: - """Linear segment by segment interpolation class for profiles.""" +def LinearProfileInterpolationSBS(profile_a, profile_b, weight): + """ + Interpolate 2 profiles linearly, segment by segment. - @staticmethod - def interpolate(profile_a, profile_b, weight): - """ - Interpolate 2 profiles. - - :param profile_a: First profile - :param profile_b: Second profile - :param weight: Weighting factor [0 .. 1]. If 0, the profile is - identical to 'a' and if 1, it is identical to b. - :return: Interpolated profile - """ - weight = np.clip(weight, 0, 1) - if not len(profile_a.shapes) == len(profile_b.shapes): - raise Exception("Number of profile shapes do not match.") + :param profile_a: First profile + :param profile_b: Second profile + :param weight: Weighting factor [0 .. 1]. If 0, the profile is identical + to 'a' and if 1, it is identical to b. + :return: Interpolated profile + """ + weight = np.clip(weight, 0, 1) + if not len(profile_a.shapes) == len(profile_b.shapes): + raise Exception("Number of profile shapes do not match.") - shapes_c = [] - for i in range(profile_a.num_shapes): - shapes_c += [Shape.linear_interpolation(profile_a.shapes[i], - profile_b.shapes[i], - weight)] + shapes_c = [] + for i in range(profile_a.num_shapes): + shapes_c += [Shape.linear_interpolation(profile_a.shapes[i], + profile_b.shapes[i], + weight)] - return Profile(shapes_c) + return Profile(shapes_c) # Varying profile class ------------------------------------------------------- @@ -1131,8 +1127,9 @@ def local_profile(self, location): segment_length = self._locations[idx + 1] - self._locations[idx] weight = (location - self._locations[idx]) / segment_length - return self._interpolation_schemes[idx].interpolate( - self._profiles[idx], self._profiles[idx + 1], weight) + return self._interpolation_schemes[idx](self._profiles[idx], + self._profiles[idx + 1], + weight) # Geometry class ------------------------------------------------------------- diff --git a/tests/test_geometry.py b/tests/test_geometry.py index 451f541..8d53979 100644 --- a/tests/test_geometry.py +++ b/tests/test_geometry.py @@ -1438,9 +1438,8 @@ def test_linear_profile_interpolation_sbs(): for i in range(5): weight = i / 4. - profile_c = geo.LinearProfileInterpolationSBS.interpolate(profile_a, - profile_b, - weight) + profile_c = geo.LinearProfileInterpolationSBS(profile_a, profile_b, + weight) check_interpolated_profile_points(profile_c, [-i, 2 * i], [8 - 2 * i, 16 - 2 * i], @@ -1448,15 +1447,11 @@ def test_linear_profile_interpolation_sbs(): # check weight clipped to valid range ----------------- - profile_c = geo.LinearProfileInterpolationSBS.interpolate(profile_a, - profile_b, - -3) + profile_c = geo.LinearProfileInterpolationSBS(profile_a, profile_b, -3) check_interpolated_profile_points(profile_c, a_0, a_1, a_2) - profile_c = geo.LinearProfileInterpolationSBS.interpolate(profile_a, - profile_b, - 42) + profile_c = geo.LinearProfileInterpolationSBS(profile_a, profile_b, 42) check_interpolated_profile_points(profile_c, b_0, b_1, b_2) @@ -1465,8 +1460,7 @@ def test_linear_profile_interpolation_sbs(): # number of shapes differ profile_d = geo.Profile([shape_b01, shape_b12, shape_a12]) with pytest.raises(Exception): - geo.LinearProfileInterpolationSBS.interpolate(profile_d, profile_b, - 0.5) + geo.LinearProfileInterpolationSBS(profile_d, profile_b, 0.5) # number of segments differ shape_b012 = geo.Shape([geo.LineSegment.construct_with_points(b_0, b_1), @@ -1474,9 +1468,7 @@ def test_linear_profile_interpolation_sbs(): profile_b2 = geo.Profile([shape_b01, shape_b012]) with pytest.raises(Exception): - geo.LinearProfileInterpolationSBS.interpolate(profile_a, - profile_b2, - 0.2) + geo.LinearProfileInterpolationSBS(profile_a, profile_b2, 0.2) # test variable profile ------------------------------------------------------- From f629017ffae54ceb551210c662482368899c3da6 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Wed, 5 Feb 2020 10:34:54 +0100 Subject: [PATCH 144/177] Adjust function name to agree with standard --- mypackage/geometry.py | 2 +- tests/test_geometry.py | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/mypackage/geometry.py b/mypackage/geometry.py index 21984dc..0179005 100644 --- a/mypackage/geometry.py +++ b/mypackage/geometry.py @@ -977,7 +977,7 @@ def rasterize(self, raster_width): # Linear profile interpolation class ------------------------------------------ -def LinearProfileInterpolationSBS(profile_a, profile_b, weight): +def linear_profile_interpolation_sbs(profile_a, profile_b, weight): """ Interpolate 2 profiles linearly, segment by segment. diff --git a/tests/test_geometry.py b/tests/test_geometry.py index 8d53979..6abaef4 100644 --- a/tests/test_geometry.py +++ b/tests/test_geometry.py @@ -1438,8 +1438,8 @@ def test_linear_profile_interpolation_sbs(): for i in range(5): weight = i / 4. - profile_c = geo.LinearProfileInterpolationSBS(profile_a, profile_b, - weight) + profile_c = geo.linear_profile_interpolation_sbs(profile_a, profile_b, + weight) check_interpolated_profile_points(profile_c, [-i, 2 * i], [8 - 2 * i, 16 - 2 * i], @@ -1447,11 +1447,11 @@ def test_linear_profile_interpolation_sbs(): # check weight clipped to valid range ----------------- - profile_c = geo.LinearProfileInterpolationSBS(profile_a, profile_b, -3) + profile_c = geo.linear_profile_interpolation_sbs(profile_a, profile_b, -3) check_interpolated_profile_points(profile_c, a_0, a_1, a_2) - profile_c = geo.LinearProfileInterpolationSBS(profile_a, profile_b, 42) + profile_c = geo.linear_profile_interpolation_sbs(profile_a, profile_b, 42) check_interpolated_profile_points(profile_c, b_0, b_1, b_2) @@ -1460,7 +1460,7 @@ def test_linear_profile_interpolation_sbs(): # number of shapes differ profile_d = geo.Profile([shape_b01, shape_b12, shape_a12]) with pytest.raises(Exception): - geo.LinearProfileInterpolationSBS(profile_d, profile_b, 0.5) + geo.linear_profile_interpolation_sbs(profile_d, profile_b, 0.5) # number of segments differ shape_b012 = geo.Shape([geo.LineSegment.construct_with_points(b_0, b_1), @@ -1468,7 +1468,7 @@ def test_linear_profile_interpolation_sbs(): profile_b2 = geo.Profile([shape_b01, shape_b012]) with pytest.raises(Exception): - geo.LinearProfileInterpolationSBS(profile_a, profile_b2, 0.2) + geo.linear_profile_interpolation_sbs(profile_a, profile_b2, 0.2) # test variable profile ------------------------------------------------------- @@ -1484,7 +1484,7 @@ def check_variable_profile_state(variable_profile, locations): def test_variable_profile_construction(): - interpol = geo.LinearProfileInterpolationSBS + interpol = geo.linear_profile_interpolation_sbs profile_a, profile_b = get_default_profiles() @@ -1543,7 +1543,7 @@ def test_variable_profile_construction(): def test_variable_profile_local_profile(): - interpol = geo.LinearProfileInterpolationSBS + interpol = geo.linear_profile_interpolation_sbs profile_a, profile_b = get_default_profiles() variable_profile = geo.VariableProfile([profile_a, profile_b, profile_a], @@ -1594,7 +1594,7 @@ def check_variable_profiles_identical(a, b): def test_geometry_construction(): profile_a, profile_b = get_default_profiles() variable_profile = geo.VariableProfile([profile_a, profile_b], [0, 1], - geo.LinearProfileInterpolationSBS) + geo.linear_profile_interpolation_sbs) radial_segment = geo.RadialHorizontalTraceSegment(1, np.pi) linear_segment = geo.LinearHorizontalTraceSegment(1) @@ -1724,7 +1724,7 @@ def test_geometry_rasterization_trace(): def test_geometry_rasterization_profile_interpolation(): - interpol = geo.LinearProfileInterpolationSBS + interpol = geo.linear_profile_interpolation_sbs a0 = [1, 0] a1 = [1, 1] From 369a456fbd5f9606cd8e6e70f27aab4c34928ca9 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Wed, 5 Feb 2020 12:19:58 +0100 Subject: [PATCH 145/177] Correct flake8 issue --- tests/test_geometry.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/test_geometry.py b/tests/test_geometry.py index 6abaef4..2414721 100644 --- a/tests/test_geometry.py +++ b/tests/test_geometry.py @@ -1593,8 +1593,10 @@ def check_variable_profiles_identical(a, b): def test_geometry_construction(): profile_a, profile_b = get_default_profiles() - variable_profile = geo.VariableProfile([profile_a, profile_b], [0, 1], - geo.linear_profile_interpolation_sbs) + variable_profile = \ + geo.VariableProfile([profile_a, profile_b], + [0, 1], + geo.linear_profile_interpolation_sbs) radial_segment = geo.RadialHorizontalTraceSegment(1, np.pi) linear_segment = geo.LinearHorizontalTraceSegment(1) From 71f82d3d6155166a8b0b6d1e3c37061b5714d352 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Thu, 6 Feb 2020 09:47:56 +0100 Subject: [PATCH 146/177] Enhance shape rasterization --- mypackage/geometry.py | 3 ++- tests/test_geometry.py | 12 ++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/mypackage/geometry.py b/mypackage/geometry.py index 0179005..7cd89e8 100644 --- a/mypackage/geometry.py +++ b/mypackage/geometry.py @@ -630,7 +630,8 @@ def rasterize(self, raster_width): raster_data = np.hstack((raster_data, segment_data)) last_point = self.segments[-1].point_end[:, np.newaxis] - raster_data = np.hstack((raster_data, last_point)) + if not ut.vector_is_close(last_point, self.segments[0].point_start): + raster_data = np.hstack((raster_data, last_point)) return raster_data diff --git a/tests/test_geometry.py b/tests/test_geometry.py index 2414721..69621fc 100644 --- a/tests/test_geometry.py +++ b/tests/test_geometry.py @@ -729,6 +729,18 @@ def test_shape_rasterization(): for point in points: assert utils.is_column_in_matrix(point, data) + assert data.shape[1] == 4 + + # no duplication if shape is closed ------------------- + + shape.add_segments( + geo.LineSegment.construct_with_points(points[3], points[0])) + + data = shape.rasterize(10) + + assert data.shape[1] == 4 + assert helpers.are_all_points_unique(data) + # exceptions ------------------------------------------ with pytest.raises(Exception): shape.rasterize(0) From 7efc864772eb859a19c5a72886adcd80c2eaff6e Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Thu, 6 Feb 2020 12:39:42 +0100 Subject: [PATCH 147/177] Add function to shape that creates multiple line segments --- mypackage/geometry.py | 31 ++++++++++++++++++++++ tests/test_geometry.py | 60 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+) diff --git a/mypackage/geometry.py b/mypackage/geometry.py index 7cd89e8..0968a2d 100644 --- a/mypackage/geometry.py +++ b/mypackage/geometry.py @@ -557,6 +557,37 @@ def segments(self): """ return self._segments + def add_line_segments(self, points): + """ + Add line segments to the shape. + + The line segments are constructed from the provided points. + + :param points: List of points / Matrix Nx2 matrix + :return: --- + """ + points = ut.to_float_array(points) + dimension = len(points.shape) + if dimension == 1: + points = points[np.newaxis, :] + elif not dimension == 2: + raise Exception("Invalid input parameter") + + if not points.shape[1] == 2: + raise Exception("Invalid point format") + + if len(self.segments) > 0: + points = np.vstack((self.segments[-1].point_end, points)) + elif points.shape[0] <= 1: + raise Exception("Insufficient number of points provided.") + + num_new_segments = len(points) - 1 + line_segments = [] + for i in range(num_new_segments): + line_segments += [LineSegment.construct_with_points(points[i], + points[i + 1])] + self.add_segments(line_segments) + def add_segments(self, segments): """ Add segments to the shape. diff --git a/tests/test_geometry.py b/tests/test_geometry.py index 69621fc..bf50683 100644 --- a/tests/test_geometry.py +++ b/tests/test_geometry.py @@ -686,6 +686,66 @@ def test_shape_segment_addition(): assert shape.num_segments == 3 # ensure shape is unmodified +def test_shape_line_segment_addition(): + shape_0 = geo.Shape() + shape_0.add_line_segments([[0, 0], [1, 0]]) + assert shape_0.num_segments == 1 + + shape_1 = geo.Shape() + shape_1.add_line_segments([[0, 0], [1, 0], [2, 0]]) + assert shape_1.num_segments == 2 + + # test possible formats to add single line segment ---- + + shape_0.add_line_segments([2, 0]) + assert shape_0.num_segments == 2 + shape_0.add_line_segments([[3, 0]]) + assert shape_0.num_segments == 3 + shape_0.add_line_segments(np.array([4, 0])) + assert shape_0.num_segments == 4 + shape_0.add_line_segments(np.array([[5, 0]])) + assert shape_0.num_segments == 5 + + # add multiple segments ------------------------------- + + shape_0.add_line_segments([[6, 0], [7, 0], [8, 0]]) + assert shape_0.num_segments == 8 + shape_0.add_line_segments(np.array([[9, 0], [10, 0], [11, 0]])) + assert shape_0.num_segments == 11 + + for i in range(11): + expected_segment = geo.LineSegment.construct_with_points([i, 0], + [i + 1, 0]) + check_segments_identical(shape_0.segments[i], expected_segment) + if i < 2: + check_segments_identical(shape_1.segments[i], expected_segment) + + # exceptions ------------------------------------------ + + shape_2 = geo.Shape() + # invalid inputs + with pytest.raises(Exception): + shape_2.add_line_segments([]) + assert shape_2.num_segments == 0 + + with pytest.raises(Exception): + shape_2.add_line_segments(None) + assert shape_2.num_segments == 0 + + # single point with empty shape + with pytest.raises(Exception): + shape_2.add_line_segments([0, 1]) + assert shape_2.num_segments == 0 + + # invalid point format + with pytest.raises(Exception): + shape_2.add_line_segments([[0, 1, 2], [1, 2, 3]]) + assert shape_2.num_segments == 0 + + +test_shape_line_segment_addition() + + def test_shape_rasterization(): points = np.array([[0, 0], [0, 1], From f8a6ec00be9acb230277ee270796b21127ba4250 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Thu, 6 Feb 2020 13:31:07 +0100 Subject: [PATCH 148/177] Rename in-place translation functions --- mypackage/geometry.py | 12 ++++++------ tests/test_geometry.py | 11 ++++------- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/mypackage/geometry.py b/mypackage/geometry.py index 0968a2d..5f80487 100644 --- a/mypackage/geometry.py +++ b/mypackage/geometry.py @@ -149,7 +149,7 @@ def rasterize(self, raster_width, num_points_excluded_end=0): return np.matmul(self._points, weight_matrix) - def translate(self, vector): + def apply_translation(self, vector): """ Apply a translation to the segment. @@ -453,7 +453,7 @@ def rasterize(self, raster_width, num_points_excluded_end=0): return data.transpose() - def translate(self, vector): + def apply_translation(self, vector): """ Apply a translation to the segment. @@ -627,11 +627,11 @@ def reflect(self, reflection_normal, distance_to_origin=0): offset = normal / np.sqrt(dot_product) * distance_to_origin - self.translate(-offset) + self.apply_translation(-offset) self.apply_transformation(householder_matrix) - self.translate(offset) + self.apply_translation(offset) - def translate(self, vector): + def apply_translation(self, vector): """ Apply a translation to the shape. @@ -639,7 +639,7 @@ def translate(self, vector): :return: --- """ for i in range(self.num_segments): - self._segments[i].translate(vector) + self._segments[i].apply_translation(vector) def rasterize(self, raster_width): """ diff --git a/tests/test_geometry.py b/tests/test_geometry.py index bf50683..b494e96 100644 --- a/tests/test_geometry.py +++ b/tests/test_geometry.py @@ -175,7 +175,7 @@ def test_line_segment_rasterization(): def test_line_segment_transformations(): # translation ----------------------------------------- segment = geo.LineSegment.construct_with_points([3, 3], [4, 5]) - segment.translate([-1, 4]) + segment.apply_translation([-1, 4]) helpers.check_vectors_identical(segment.point_start, np.array([2, 7])) helpers.check_vectors_identical(segment.point_end, np.array([3, 9])) @@ -492,8 +492,8 @@ def test_arc_segment_transformations(): False) segment_ccw = geo.ArcSegment.construct_with_points([3, 3], [5, 5], [5, 3], True) - segment_cw.translate([-1, 4]) - segment_ccw.translate([-1, 4]) + segment_cw.apply_translation([-1, 4]) + segment_ccw.apply_translation([-1, 4]) exp_start = [2, 7] exp_end = [4, 9] @@ -743,9 +743,6 @@ def test_shape_line_segment_addition(): assert shape_2.num_segments == 0 -test_shape_line_segment_addition() - - def test_shape_rasterization(): points = np.array([[0, 0], [0, 1], @@ -825,7 +822,7 @@ def check_point(point, point_ref, translation): shape = copy.deepcopy(shape_ref) # apply translation - shape.translate(translation) + shape.apply_translation(translation) arc_segment = shape.segments[0] arc_segment_ref = shape_ref.segments[0] From 9f710f837a27d624239dc9c14aa94a751f9f180b Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Thu, 6 Feb 2020 14:15:04 +0100 Subject: [PATCH 149/177] Add transformation functions to line segment --- mypackage/geometry.py | 54 +++++++++++++++++++++++---------- tests/test_geometry.py | 68 ++++++++++++++++++++++++++++++++---------- 2 files changed, 91 insertions(+), 31 deletions(-) diff --git a/mypackage/geometry.py b/mypackage/geometry.py index 5f80487..295b3f2 100644 --- a/mypackage/geometry.py +++ b/mypackage/geometry.py @@ -1,11 +1,13 @@ """Provides classes to define lines and surfaces.""" -import numpy as np -import math import mypackage._utility as ut import mypackage.transformations as tf from scipy.spatial.transform import Rotation as Rot +import copy +import math +import numpy as np + # LineSegment ----------------------------------------------------------------- @@ -117,6 +119,15 @@ def apply_transformation(self, matrix): self._points = np.matmul(matrix, self._points) self._calculate_length() + def apply_translation(self, vector): + """ + Apply a translation to the segment. + + :param vector: Translation vector + :return: --- + """ + self._points += np.ndarray((2, 1), float, np.array(vector, float)) + def rasterize(self, raster_width, num_points_excluded_end=0): """ Create an array of points that describe the segments contour. @@ -149,14 +160,27 @@ def rasterize(self, raster_width, num_points_excluded_end=0): return np.matmul(self._points, weight_matrix) - def apply_translation(self, vector): + def transform(self, matrix): """ - Apply a translation to the segment. + Get a transformed copy of the segment. + + :param matrix: Transformation matrix + :return: Transformed copy + """ + new_segment = copy.deepcopy(self) + new_segment.apply_transformation(matrix) + return new_segment + + def translate(self, vector): + """ + Get a translated copy of the segment. :param vector: Translation vector - :return: --- + :return: Transformed copy """ - self._points += np.ndarray((2, 1), float, np.array(vector, float)) + new_segment = copy.deepcopy(self) + new_segment.apply_translation(vector) + return new_segment # ArcSegment ------------------------------------------------------------------ @@ -415,6 +439,15 @@ def apply_transformation(self, matrix): self._sign_arc_winding *= tf.reflection_sign(matrix) self._calculate_arc_parameters() + def apply_translation(self, vector): + """ + Apply a translation to the segment. + + :param vector: Translation vector + :return: --- + """ + self._points += np.ndarray((2, 1), float, np.array(vector, float)) + def rasterize(self, raster_width, num_points_excluded_end=0): """ Create an array of points that describe the segments contour. @@ -453,15 +486,6 @@ def rasterize(self, raster_width, num_points_excluded_end=0): return data.transpose() - def apply_translation(self, vector): - """ - Apply a translation to the segment. - - :param vector: Translation vector - :return: --- - """ - self._points += np.ndarray((2, 1), float, np.array(vector, float)) - # Shape class ----------------------------------------------------------------- diff --git a/tests/test_geometry.py b/tests/test_geometry.py index b494e96..05438c3 100644 --- a/tests/test_geometry.py +++ b/tests/test_geometry.py @@ -175,11 +175,20 @@ def test_line_segment_rasterization(): def test_line_segment_transformations(): # translation ----------------------------------------- segment = geo.LineSegment.construct_with_points([3, 3], [4, 5]) - segment.apply_translation([-1, 4]) + segment_2 = segment.translate([-1, 4]) - helpers.check_vectors_identical(segment.point_start, np.array([2, 7])) - helpers.check_vectors_identical(segment.point_end, np.array([3, 9])) - assert math.isclose(segment.length, np.sqrt(5)) + # original segment not modified + helpers.check_vectors_identical(segment.point_start, np.array([3, 3])) + helpers.check_vectors_identical(segment.point_end, np.array([4, 5])) + + # check new segment + helpers.check_vectors_identical(segment_2.point_start, np.array([2, 7])) + helpers.check_vectors_identical(segment_2.point_end, np.array([3, 9])) + assert math.isclose(segment_2.length, np.sqrt(5)) + + # apply same transformation in place + segment.apply_translation([-1, 4]) + check_segments_identical(segment, segment_2) # 45 degree rotation ---------------------------------- s = np.sin(np.pi / 4.) @@ -187,36 +196,63 @@ def test_line_segment_transformations(): rotation_matrix = [[c, -s], [s, c]] segment = geo.LineSegment.construct_with_points([2, 2], [3, 6]) - segment.apply_transformation(rotation_matrix) + segment_2 = segment.transform(rotation_matrix) + + # original segment not modified + helpers.check_vectors_identical(segment.point_start, np.array([2, 2])) + helpers.check_vectors_identical(segment.point_end, np.array([3, 6])) + # check new segment exp_start = [0, np.sqrt(8)] exp_end = np.matmul(rotation_matrix, [3, 6]) - helpers.check_vectors_identical(segment.point_start, exp_start) - helpers.check_vectors_identical(segment.point_end, exp_end) - assert math.isclose(segment.length, np.sqrt(17)) + helpers.check_vectors_identical(segment_2.point_start, exp_start) + helpers.check_vectors_identical(segment_2.point_end, exp_end) + assert math.isclose(segment_2.length, np.sqrt(17)) + + # apply same transformation in place + segment.apply_transformation(rotation_matrix) + check_segments_identical(segment, segment_2) # reflection at 45 degree line ------------------------ v = np.array([-1, 1], dtype=float) reflection_matrix = np.identity(2) - 2 / np.dot(v, v) * np.outer(v, v) segment = geo.LineSegment.construct_with_points([-1, 3], [6, 1]) - segment.apply_transformation(reflection_matrix) + segment_2 = segment.transform(reflection_matrix) + + # original segment not modified + helpers.check_vectors_identical(segment.point_start, np.array([-1, 3])) + helpers.check_vectors_identical(segment.point_end, np.array([6, 1])) - helpers.check_vectors_identical(segment.point_start, [3, -1]) - helpers.check_vectors_identical(segment.point_end, [1, 6]) - assert math.isclose(segment.length, np.sqrt(53)) + # check new segment + helpers.check_vectors_identical(segment_2.point_start, [3, -1]) + helpers.check_vectors_identical(segment_2.point_end, [1, 6]) + assert math.isclose(segment_2.length, np.sqrt(53)) + + # apply same transformation in place + segment.apply_transformation(reflection_matrix) + check_segments_identical(segment, segment_2) # scaling --------------------------------------------- scale_matrix = [[4, 0], [0, 0.5]] segment = geo.LineSegment.construct_with_points([-2, 2], [1, 4]) - segment.apply_transformation(scale_matrix) + segment_2 = segment.transform(scale_matrix) + + # original segment not modified + helpers.check_vectors_identical(segment.point_start, np.array([-2, 2])) + helpers.check_vectors_identical(segment.point_end, np.array([1, 4])) - helpers.check_vectors_identical(segment.point_start, [-8, 1]) - helpers.check_vectors_identical(segment.point_end, [4, 2]) + # check new segment + helpers.check_vectors_identical(segment_2.point_start, [-8, 1]) + helpers.check_vectors_identical(segment_2.point_end, [4, 2]) # length changes due to scaling! - assert math.isclose(segment.length, np.sqrt(145)) + assert math.isclose(segment_2.length, np.sqrt(145)) + + # apply same transformation in place + segment.apply_transformation(scale_matrix) + check_segments_identical(segment, segment_2) # exceptions ------------------------------------------ From 84d80bce947ef6c5e09d41302c3f6ea3e6ff1043 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Thu, 6 Feb 2020 14:35:38 +0100 Subject: [PATCH 150/177] Add transformation functions to arc segment --- mypackage/geometry.py | 22 +++++++++++ tests/test_geometry.py | 86 +++++++++++++++++++++++++++++++++--------- 2 files changed, 91 insertions(+), 17 deletions(-) diff --git a/mypackage/geometry.py b/mypackage/geometry.py index 295b3f2..d393f58 100644 --- a/mypackage/geometry.py +++ b/mypackage/geometry.py @@ -486,6 +486,28 @@ def rasterize(self, raster_width, num_points_excluded_end=0): return data.transpose() + def transform(self, matrix): + """ + Get a transformed copy of the segment. + + :param matrix: Transformation matrix + :return: Transformed copy + """ + new_segment = copy.deepcopy(self) + new_segment.apply_transformation(matrix) + return new_segment + + def translate(self, vector): + """ + Get a translated copy of the segment. + + :param vector: Translation vector + :return: Transformed copy + """ + new_segment = copy.deepcopy(self) + new_segment.apply_translation(vector) + return new_segment + # Shape class ----------------------------------------------------------------- diff --git a/tests/test_geometry.py b/tests/test_geometry.py index 05438c3..e9cc1a5 100644 --- a/tests/test_geometry.py +++ b/tests/test_geometry.py @@ -260,6 +260,8 @@ def test_line_segment_transformations(): zero_matrix = np.zeros((2, 2)) with pytest.raises(Exception): segment.apply_transformation(zero_matrix) + with pytest.raises(Exception): + segment.transform(zero_matrix) def test_line_segment_interpolation(): @@ -528,9 +530,17 @@ def test_arc_segment_transformations(): False) segment_ccw = geo.ArcSegment.construct_with_points([3, 3], [5, 5], [5, 3], True) - segment_cw.apply_translation([-1, 4]) - segment_ccw.apply_translation([-1, 4]) + segment_cw_2 = segment_cw.translate([-1, 4]) + segment_ccw_2 = segment_ccw.translate([-1, 4]) + + # original segment not modified + check_arc_segment_values(segment_cw, [3, 3], [5, 5], [5, 3], + False, 2, 0.5 * np.pi, np.pi) + check_arc_segment_values(segment_ccw, [3, 3], [5, 5], [5, 3], + True, 2, 1.5 * np.pi, 3 * np.pi) + + # check new segment exp_start = [2, 7] exp_end = [4, 9] exp_center = [4, 7] @@ -540,10 +550,18 @@ def test_arc_segment_transformations(): exp_arc_length_cw = np.pi exp_arc_length_ccw = 3 * np.pi - check_arc_segment_values(segment_cw, exp_start, exp_end, exp_center, False, - exp_radius, exp_angle_cw, exp_arc_length_cw) - check_arc_segment_values(segment_ccw, exp_start, exp_end, exp_center, True, - exp_radius, exp_angle_ccw, exp_arc_length_ccw) + check_arc_segment_values(segment_cw_2, exp_start, exp_end, exp_center, + False, exp_radius, exp_angle_cw, + exp_arc_length_cw) + check_arc_segment_values(segment_ccw_2, exp_start, exp_end, exp_center, + True, exp_radius, exp_angle_ccw, + exp_arc_length_ccw) + + # apply same transformation in place + segment_cw.apply_translation([-1, 4]) + segment_ccw.apply_translation([-1, 4]) + check_segments_identical(segment_cw_2, segment_cw) + check_segments_identical(segment_ccw_2, segment_ccw) # 45 degree rotation ---------------------------------- s = np.sin(np.pi / 4.) @@ -554,17 +572,33 @@ def test_arc_segment_transformations(): False) segment_ccw = geo.ArcSegment.construct_with_points([3, 3], [5, 5], [5, 3], True) - segment_cw.apply_transformation(rotation_matrix) - segment_ccw.apply_transformation(rotation_matrix) + segment_cw_2 = segment_cw.transform(rotation_matrix) + segment_ccw_2 = segment_ccw.transform(rotation_matrix) + + # original segment not modified + check_arc_segment_values(segment_cw, [3, 3], [5, 5], [5, 3], + False, 2, 0.5 * np.pi, np.pi) + check_arc_segment_values(segment_ccw, [3, 3], [5, 5], [5, 3], + True, 2, 1.5 * np.pi, 3 * np.pi) + + # check new segment exp_start = [0, np.sqrt(18)] exp_end = [0, np.sqrt(50)] exp_center = np.matmul(rotation_matrix, [5, 3]) - check_arc_segment_values(segment_cw, exp_start, exp_end, exp_center, False, - exp_radius, exp_angle_cw, exp_arc_length_cw) - check_arc_segment_values(segment_ccw, exp_start, exp_end, exp_center, True, - exp_radius, exp_angle_ccw, exp_arc_length_ccw) + check_arc_segment_values(segment_cw_2, exp_start, exp_end, exp_center, + False, exp_radius, exp_angle_cw, + exp_arc_length_cw) + check_arc_segment_values(segment_ccw_2, exp_start, exp_end, exp_center, + True, exp_radius, exp_angle_ccw, + exp_arc_length_ccw) + + # apply same transformation in place + segment_cw.apply_transformation(rotation_matrix) + segment_ccw.apply_transformation(rotation_matrix) + check_segments_identical(segment_cw_2, segment_cw) + check_segments_identical(segment_ccw_2, segment_ccw) # reflection at 45 degree line ------------------------ v = np.array([-1, 1], dtype=float) @@ -574,20 +608,34 @@ def test_arc_segment_transformations(): False) segment_ccw = geo.ArcSegment.construct_with_points([3, 2], [5, 4], [5, 2], True) - segment_cw.apply_transformation(reflection_matrix) - segment_ccw.apply_transformation(reflection_matrix) + segment_cw_2 = segment_cw.transform(reflection_matrix) + segment_ccw_2 = segment_ccw.transform(reflection_matrix) + + # original segment not modified + check_arc_segment_values(segment_cw, [3, 2], [5, 4], [5, 2], + False, 2, 0.5 * np.pi, np.pi) + check_arc_segment_values(segment_ccw, [3, 2], [5, 4], [5, 2], + True, 2, 1.5 * np.pi, 3 * np.pi) + + # check new segment exp_start = [2, 3] exp_end = [4, 5] exp_center = [2, 5] # Reflection must change winding! - check_arc_segment_values(segment_cw, exp_start, exp_end, exp_center, True, - exp_radius, exp_angle_cw, exp_arc_length_cw) - check_arc_segment_values(segment_ccw, exp_start, exp_end, exp_center, + check_arc_segment_values(segment_cw_2, exp_start, exp_end, exp_center, + True, exp_radius, exp_angle_cw, exp_arc_length_cw) + check_arc_segment_values(segment_ccw_2, exp_start, exp_end, exp_center, False, exp_radius, exp_angle_ccw, exp_arc_length_ccw) + # apply same transformation in place + segment_cw.apply_transformation(reflection_matrix) + segment_ccw.apply_transformation(reflection_matrix) + check_segments_identical(segment_cw_2, segment_cw) + check_segments_identical(segment_ccw_2, segment_ccw) + # scaling both coordinates equally -------------------- scaling_matrix = [[4, 0], [0, 4]] @@ -643,6 +691,8 @@ def test_arc_segment_transformations(): # transformation distorts arc segment = geo.ArcSegment.construct_with_points([3, 2], [5, 4], [5, 2], False) + with pytest.raises(Exception): + segment.transform(scaling_matrix) with pytest.raises(Exception): segment.apply_transformation(scaling_matrix) @@ -650,6 +700,8 @@ def test_arc_segment_transformations(): segment = geo.ArcSegment.construct_with_points([3, 2], [5, 4], [5, 2], False) zero_matrix = np.zeros((2, 2)) + with pytest.raises(Exception): + segment.transform(zero_matrix) with pytest.raises(Exception): segment.apply_transformation(zero_matrix) From 9d3629c9a718862357956205fa8aae7c06556680 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Thu, 6 Feb 2020 14:49:12 +0100 Subject: [PATCH 151/177] Add transformation functions to shape --- mypackage/geometry.py | 22 ++++++++++++++++++++++ tests/test_geometry.py | 35 +++++++++++++++++++++++++++-------- 2 files changed, 49 insertions(+), 8 deletions(-) diff --git a/mypackage/geometry.py b/mypackage/geometry.py index d393f58..8b01a05 100644 --- a/mypackage/geometry.py +++ b/mypackage/geometry.py @@ -711,6 +711,28 @@ def rasterize(self, raster_width): raster_data = np.hstack((raster_data, last_point)) return raster_data + def transform(self, matrix): + """ + Get a transformed copy of the shape. + + :param matrix: Transformation matrix + :return: Transformed copy + """ + new_shape = copy.deepcopy(self) + new_shape.apply_transformation(matrix) + return new_shape + + def translate(self, vector): + """ + Get a translated copy of the shape. + + :param vector: Translation vector + :return: Transformed copy + """ + new_shape = copy.deepcopy(self) + new_shape.apply_translation(vector) + return new_shape + # Profile class --------------------------------------------------------------- diff --git a/tests/test_geometry.py b/tests/test_geometry.py index e9cc1a5..5f6186b 100644 --- a/tests/test_geometry.py +++ b/tests/test_geometry.py @@ -907,10 +907,12 @@ def check_point(point, point_ref, translation): translation = [3, 4] shape_ref = default_test_shape() - shape = copy.deepcopy(shape_ref) # apply translation - shape.apply_translation(translation) + shape = shape_ref.translate(translation) + + # original shape unchanged + check_shapes_identical(shape_ref, default_test_shape()) arc_segment = shape.segments[0] arc_segment_ref = shape_ref.segments[0] @@ -932,9 +934,13 @@ def check_point(point, point_ref, translation): check_point(line_segment.point_end, line_segment_ref.point_end, translation) + # apply same transformation in place + shape_ref.apply_translation(translation) + check_shapes_identical(shape_ref, shape) + def test_shape_transformation(): - # without reflection + # without reflection ---------------------------------- def check_point_rotation(point, point_ref): assert point[0] == point_ref[1] assert point[1] == -point_ref[0] @@ -942,10 +948,12 @@ def check_point_rotation(point, point_ref): rotation_matrix = np.array([[0, 1], [-1, 0]]) shape_ref = default_test_shape() - shape = copy.deepcopy(shape_ref) # apply transformation - shape.apply_transformation(rotation_matrix) + shape = shape_ref.transform(rotation_matrix) + + # original shape unchanged + check_shapes_identical(shape_ref, default_test_shape()) arc_segment = shape.segments[0] arc_segment_ref = shape_ref.segments[0] @@ -964,17 +972,24 @@ def check_point_rotation(point, point_ref): line_segment_ref.point_start) check_point_rotation(line_segment.point_end, line_segment_ref.point_end) - # with reflection + # apply same transformation in place + shape_ref.apply_transformation(rotation_matrix) + check_shapes_identical(shape_ref, shape) + + # with reflection ------------------------------------- def check_point_reflection(point, point_ref): assert point[0] == point_ref[1] assert point[1] == point_ref[0] reflection_matrix = np.array([[0, 1], [1, 0]]) - shape = copy.deepcopy(shape_ref) + shape_ref = default_test_shape() # apply transformation - shape.apply_transformation(reflection_matrix) + shape = shape_ref.transform(reflection_matrix) + + # original shape unchanged + check_shapes_identical(shape_ref, default_test_shape()) arc_segment = shape.segments[0] arc_segment_ref = shape_ref.segments[0] @@ -994,6 +1009,10 @@ def check_point_reflection(point, point_ref): line_segment_ref.point_start) check_point_reflection(line_segment.point_end, line_segment_ref.point_end) + # apply same transformation in place + shape_ref.apply_transformation(reflection_matrix) + check_shapes_identical(shape_ref, shape) + def check_reflected_point(point, reflected_point, axis_offset, direction_reflection_axis): From a9adb608c412483f8d20e2fbf0f3f309e8a5c966 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Thu, 6 Feb 2020 16:19:14 +0100 Subject: [PATCH 152/177] Add reflect functions to shape --- mypackage/geometry.py | 19 ++++++++++++++++--- tests/test_geometry.py | 28 +++++++++++++++++----------- 2 files changed, 33 insertions(+), 14 deletions(-) diff --git a/mypackage/geometry.py b/mypackage/geometry.py index 8b01a05..2de37c9 100644 --- a/mypackage/geometry.py +++ b/mypackage/geometry.py @@ -657,12 +657,12 @@ def apply_transformation(self, transformation_matrix): for i in range(self.num_segments): self._segments[i].apply_transformation(transformation_matrix) - def reflect(self, reflection_normal, distance_to_origin=0): + def apply_reflection(self, reflection_normal, distance_to_origin=0): """ Apply a reflection at the given axis to the shape. - :param reflection_normal: Normal of the reflection axis - :param distance_to_origin: Distance of the reflection axis to the + :param reflection_normal: Normal of the line of reflection + :param distance_to_origin: Distance of the line of reflection to the origin :return: --- """ @@ -711,6 +711,19 @@ def rasterize(self, raster_width): raster_data = np.hstack((raster_data, last_point)) return raster_data + def reflect(self, reflection_normal, distance_to_origin=0): + """ + Get a reflected copy of the shape. + + :param reflection_normal: Normal of the line of reflection + :param distance_to_origin: Distance of the line of reflection to the + origin + :return: --- + """ + new_shape = copy.deepcopy(self) + new_shape.apply_reflection(reflection_normal, distance_to_origin) + return new_shape + def transform(self, matrix): """ Get a transformed copy of the shape. diff --git a/tests/test_geometry.py b/tests/test_geometry.py index 5f6186b..f2f6dcd 100644 --- a/tests/test_geometry.py +++ b/tests/test_geometry.py @@ -1025,7 +1025,7 @@ def check_reflected_point(point, reflected_point, axis_offset, assert np.abs(determinant) < 1E-8 -def shape_reflect_testcase(normal, distance_to_origin): +def shape_reflection_testcase(normal, distance_to_origin): direction_reflection_axis = np.array([normal[1], -normal[0]]) normal_length = np.linalg.norm(normal) unit_normal = np.array(normal) / normal_length @@ -1034,8 +1034,10 @@ def shape_reflect_testcase(normal, distance_to_origin): shape = default_test_shape() # create reflected shape - shape_reflected = copy.deepcopy(shape) - shape_reflected.reflect(normal, distance_to_origin) + shape_reflected = shape.reflect(normal, distance_to_origin) + + # original shape is not modified + check_shapes_identical(shape, default_test_shape()) arc_segment = shape.segments[0] arc_segment_ref = shape_reflected.segments[0] @@ -1065,15 +1067,19 @@ def shape_reflect_testcase(normal, distance_to_origin): offset, direction_reflection_axis) + # apply same reflection in place + shape.apply_reflection(normal, distance_to_origin) + check_shapes_identical(shape, shape_reflected) + -def test_shape_reflect(): - shape_reflect_testcase([2, 1], np.linalg.norm([2, 1])) - shape_reflect_testcase([0, 1], 5) - shape_reflect_testcase([1, 0], 3) - shape_reflect_testcase([1, 0], -3) - shape_reflect_testcase([-7, 2], 4.12) - shape_reflect_testcase([-7, -2], 4.12) - shape_reflect_testcase([7, -2], 4.12) +def test_shape_reflection(): + shape_reflection_testcase([2, 1], np.linalg.norm([2, 1])) + shape_reflection_testcase([0, 1], 5) + shape_reflection_testcase([1, 0], 3) + shape_reflection_testcase([1, 0], -3) + shape_reflection_testcase([-7, 2], 4.12) + shape_reflection_testcase([-7, -2], 4.12) + shape_reflection_testcase([7, -2], 4.12) def interpolation_nearest(segment_a, segment_b, weight): From 74891a91edb0b0ec1e6ee56a180b1ea426bacf55 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Fri, 7 Feb 2020 10:24:35 +0100 Subject: [PATCH 153/177] Add alternative reflection methods to shape --- mypackage/geometry.py | 42 +++++++++++++++++++++- tests/test_geometry.py | 82 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 123 insertions(+), 1 deletion(-) diff --git a/mypackage/geometry.py b/mypackage/geometry.py index 2de37c9..23aee85 100644 --- a/mypackage/geometry.py +++ b/mypackage/geometry.py @@ -666,7 +666,10 @@ def apply_reflection(self, reflection_normal, distance_to_origin=0): origin :return: --- """ - normal = np.array(reflection_normal, float) + normal = ut.to_float_array(reflection_normal) + if ut.vector_is_close(normal, ut.to_float_array([0, 0])): + raise Exception("Normal has no length.") + dot_product = np.dot(normal, normal) outer_product = np.outer(normal, normal) householder_matrix = np.identity(2) - 2 / dot_product * outer_product @@ -677,6 +680,31 @@ def apply_reflection(self, reflection_normal, distance_to_origin=0): self.apply_transformation(householder_matrix) self.apply_translation(offset) + def apply_reflection_across_line(self, point_start, point_end): + """ + Apply a reflection across a line. + + :param point_start: Line of reflection's start point + :param point_start: Line of reflection's end point + :return: --- + """ + point_start = ut.to_float_array(point_start) + point_end = ut.to_float_array(point_end) + + if ut.vector_is_close(point_start, point_end): + raise Exception("Line start and end point are identical.") + + vector = point_end - point_start + length_vector = np.linalg.norm(vector) + + line_distance_origin = np.abs( + point_start[1] * point_end[0] - point_start[0] * point_end[ + 1]) / length_vector + + normal = ut.to_float_array([-vector[1], vector[0]]) + + self.apply_reflection(normal, line_distance_origin) + def apply_translation(self, vector): """ Apply a translation to the shape. @@ -724,6 +752,18 @@ def reflect(self, reflection_normal, distance_to_origin=0): new_shape.apply_reflection(reflection_normal, distance_to_origin) return new_shape + def reflect_across_line(self, point_start, point_end): + """ + Get a reflected copy across a line. + + :param point_start: Line of reflection's start point + :param point_start: Line of reflection's end point + :return + """ + new_shape = copy.deepcopy(self) + new_shape.apply_reflection_across_line(point_start, point_end) + return new_shape + def transform(self, matrix): """ Get a transformed copy of the shape. diff --git a/tests/test_geometry.py b/tests/test_geometry.py index f2f6dcd..9550ed6 100644 --- a/tests/test_geometry.py +++ b/tests/test_geometry.py @@ -1081,6 +1081,88 @@ def test_shape_reflection(): shape_reflection_testcase([-7, -2], 4.12) shape_reflection_testcase([7, -2], 4.12) + # exceptions ------------------------------------------ + shape = default_test_shape() + + with pytest.raises(Exception): + shape.reflect([0, 0], 2) + with pytest.raises(Exception): + shape.apply_reflection([0, 0]) + + +def check_point_reflected_across_line(point, reflected_point, point_start, + point_end): + """Check if the midpoint lies on the reflection axis.""" + vec_original_reflected = reflected_point - point + mid_point = point + 0.5 * vec_original_reflected + + vec_start_mid = mid_point - point_start + vec_start_end = point_end - point_start + + determinant = np.linalg.det([vec_start_end, vec_start_mid]) + assert np.abs(determinant) < 1E-8 + + +def shape_reflection_across_line_testcase(point_start, point_end): + point_start = np.array(point_start, float) + point_end = np.array(point_end, float) + + shape = default_test_shape() + + # create reflected shape + shape_reflected = shape.reflect_across_line(point_start, point_end) + + # original shape is not modified + check_shapes_identical(shape, default_test_shape()) + + arc_segment = shape.segments[0] + arc_segment_ref = shape_reflected.segments[0] + line_segment = shape.segments[1] + line_segment_ref = shape_reflected.segments[1] + + # check reflected points + check_point_reflected_across_line(arc_segment.point_start, + arc_segment_ref.point_start, + point_start, + point_end) + check_point_reflected_across_line(arc_segment.point_end, + arc_segment_ref.point_end, + point_start, + point_end) + check_point_reflected_across_line(arc_segment.point_center, + arc_segment_ref.point_center, + point_start, + point_end) + + check_point_reflected_across_line(line_segment.point_start, + line_segment_ref.point_start, + point_start, + point_end) + check_point_reflected_across_line(line_segment.point_end, + line_segment_ref.point_end, + point_start, + point_end) + + # apply same reflection in place + shape.apply_reflection_across_line(point_start, point_end) + check_shapes_identical(shape, shape_reflected) + + +def test_shape_reflection_across_line(): + shape_reflection_across_line_testcase([0, 0], [0, 1]) + shape_reflection_across_line_testcase([0, 0], [1, 0]) + shape_reflection_across_line_testcase([-3, 2.5], [31.53, -23.44]) + shape_reflection_across_line_testcase([7, 8], [9, 10]) + shape_reflection_across_line_testcase([-4.26, -23.1], [-8, -0.12]) + + # exceptions ------------------------------------------ + shape = default_test_shape() + + with pytest.raises(Exception): + shape.reflect_across_line([2, 5], [2, 5]) + with pytest.raises(Exception): + shape.apply_reflection_across_line([-3, 2], [-3, 2]) + def interpolation_nearest(segment_a, segment_b, weight): if weight > 0.5: From 9a931895adcfac233921257ce43ddd220ab22a03 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Fri, 7 Feb 2020 11:26:43 +0100 Subject: [PATCH 154/177] Fix a reflection bug - add test --- mypackage/geometry.py | 5 ++++- tests/test_geometry.py | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/mypackage/geometry.py b/mypackage/geometry.py index 23aee85..0ebf8e4 100644 --- a/mypackage/geometry.py +++ b/mypackage/geometry.py @@ -701,7 +701,10 @@ def apply_reflection_across_line(self, point_start, point_end): point_start[1] * point_end[0] - point_start[0] * point_end[ 1]) / length_vector - normal = ut.to_float_array([-vector[1], vector[0]]) + if tf.point_left_of_line([0, 0], point_start, point_end) > 0: + normal = ut.to_float_array([vector[1], -vector[0]]) + else: + normal = ut.to_float_array([-vector[1], vector[0]]) self.apply_reflection(normal, line_distance_origin) diff --git a/tests/test_geometry.py b/tests/test_geometry.py index 9550ed6..14ec667 100644 --- a/tests/test_geometry.py +++ b/tests/test_geometry.py @@ -1154,6 +1154,7 @@ def test_shape_reflection_across_line(): shape_reflection_across_line_testcase([-3, 2.5], [31.53, -23.44]) shape_reflection_across_line_testcase([7, 8], [9, 10]) shape_reflection_across_line_testcase([-4.26, -23.1], [-8, -0.12]) + shape_reflection_across_line_testcase([-2, 1], [2, -4.5]) # exceptions ------------------------------------------ shape = default_test_shape() From c1afbec6ea32fd9dc0d14223163e871299dd30ca Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Mon, 10 Feb 2020 08:02:40 +0100 Subject: [PATCH 155/177] Remove second parameter from segment rasterization methods --- mypackage/geometry.py | 30 ++++++++---------------------- tests/test_geometry.py | 10 ---------- 2 files changed, 8 insertions(+), 32 deletions(-) diff --git a/mypackage/geometry.py b/mypackage/geometry.py index 0ebf8e4..7d2d4e3 100644 --- a/mypackage/geometry.py +++ b/mypackage/geometry.py @@ -2,7 +2,6 @@ import mypackage._utility as ut import mypackage.transformations as tf -from scipy.spatial.transform import Rotation as Rot import copy import math @@ -128,7 +127,7 @@ def apply_translation(self, vector): """ self._points += np.ndarray((2, 1), float, np.array(vector, float)) - def rasterize(self, raster_width, num_points_excluded_end=0): + def rasterize(self, raster_width): """ Create an array of points that describe the segments contour. @@ -137,10 +136,6 @@ def rasterize(self, raster_width, num_points_excluded_end=0): raster points. :param raster_width: The desired distance between two raster points - :param num_points_excluded_end: Specifies how many points from the - end should be excluded from the rasterization. The main purpose of - this parameter is to avoid point duplication when rasterizing - multiple segments. :return: Array of contour points """ if not raster_width > 0: @@ -152,10 +147,7 @@ def rasterize(self, raster_width, num_points_excluded_end=0): # normalized effective raster width nerw = 1. / num_raster_segments - range_modifier = (0.5 - np.floor( - np.abs(num_points_excluded_end))) * nerw - - multiplier = np.arange(0, 1 + range_modifier, nerw) + multiplier = np.arange(0, 1 + 0.5 * nerw, nerw) weight_matrix = np.array([1 - multiplier, multiplier]) return np.matmul(self._points, weight_matrix) @@ -448,7 +440,7 @@ def apply_translation(self, vector): """ self._points += np.ndarray((2, 1), float, np.array(vector, float)) - def rasterize(self, raster_width, num_points_excluded_end=0): + def rasterize(self, raster_width): """ Create an array of points that describe the segments contour. @@ -457,10 +449,6 @@ def rasterize(self, raster_width, num_points_excluded_end=0): raster points. :param raster_width: The desired distance between two raster points - :param num_points_excluded_end: Specifies how many points from the - end should be excluded from the rasterization. The main purpose of - this parameter is to avoid point duplication when rasterizing - multiple segments. :return: Array of contour points """ point_start = self.point_start @@ -474,13 +462,11 @@ def rasterize(self, raster_width, num_points_excluded_end=0): num_raster_segments = int(np.round(self._arc_length / raster_width)) delta_angle = self._arc_angle / num_raster_segments - range_modifier = (0.5 - np.floor( - np.abs(num_points_excluded_end))) * delta_angle - max_angle = self._sign_arc_winding * (self._arc_angle + range_modifier) - + max_angle = self._sign_arc_winding * (self._arc_angle + + 0.5 * delta_angle) angles = np.arange(0, max_angle, self._sign_arc_winding * delta_angle) - rotation_matrices = Rot.from_euler('z', angles).as_dcm()[:, 0:2, 0:2] + rotation_matrices = tf.rotation_matrix_z(angles)[:, 0:2, 0:2] data = np.matmul(rotation_matrices, vec_center_start) + point_center @@ -734,8 +720,8 @@ def rasterize(self, raster_width): raster_data = np.empty([2, 0]) for i in range(self.num_segments): - segment_data = self.segments[i].rasterize(raster_width, 1) - raster_data = np.hstack((raster_data, segment_data)) + segment_data = self.segments[i].rasterize(raster_width) + raster_data = np.hstack((raster_data, segment_data[:, :-1])) last_point = self.segments[-1].point_end[:, np.newaxis] if not ut.vector_is_close(last_point, self.segments[0].point_start): diff --git a/tests/test_geometry.py b/tests/test_geometry.py index 14ec667..37a653b 100644 --- a/tests/test_geometry.py +++ b/tests/test_geometry.py @@ -95,16 +95,6 @@ def default_segment_rasterization_tests(segment, raster_width, point_start, # check that there are no duplicate points assert helpers.are_all_points_unique(data) - # check rasterization with excluded points - data_m2 = segment.rasterize(raster_width, 2) - - num_points_m2 = data_m2.shape[1] - - assert num_points - 2 == num_points_m2 - - for i in range(num_points_m2): - helpers.check_vectors_identical(data[:, i], data_m2[:, i]) - # check that rasterization with to large raster width still works data_200 = segment.rasterize(200) From 1f62cb24d78fc4d215e46d3c92db5db5234a09c2 Mon Sep 17 00:00:00 2001 From: vhirtham Date: Wed, 5 Feb 2020 11:07:38 +0100 Subject: [PATCH 156/177] Delete outdated tutorial --- tutorials/shapes.ipynb | 294 ----------------------------------------- 1 file changed, 294 deletions(-) delete mode 100644 tutorials/shapes.ipynb diff --git a/tutorials/shapes.ipynb b/tutorials/shapes.ipynb deleted file mode 100644 index fa261a5..0000000 --- a/tutorials/shapes.ipynb +++ /dev/null @@ -1,294 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "from mpl_toolkits.mplot3d import Axes3D # noqa: F401 unused import\n", - "import matplotlib.pyplot as plt\n", - "import numpy as np\n", - "import copy\n", - "\n", - "import mypackage.geometry as geo\n", - "import mypackage.point_cloud_generator as pcg\n", - "import mypackage.transformations as tr\n" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[]" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "segment0 = geo.LineSegment.construct_from_points([0, 0], [1, 0])\n", - "segment0_data = segment0.rasterize(0.1)\n", - "plt.plot(segment0_data[0], segment0_data[1], \"o\")" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[]" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "segment1 = geo.ArcSegment.construct_from_points([1,0], [2,-1], [2, 0])\n", - "segment1_data = segment1.rasterize(0.1)\n", - "plt.plot(segment1_data[0], segment1_data[1], \"o\")" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[]" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "segment2 = geo.LineSegment.construct_from_points([2, -1], [2, -2])\n", - "segment3 = geo.LineSegment.construct_from_points([2, -2], [0, -2])\n", - "\n", - "shape0 = geo.Shape2D([segment0, segment1, segment2, segment3])\n", - "shape0_data = shape0.rasterize(0.1)\n", - "\n", - "plt.plot(shape0_data[0], shape0_data[1],\"x-\")" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[]" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "plt.plot(shape0_data[0], shape0_data[1],\"x-\")\n", - "shape0.translate([-2.5, 0.5])\n", - "shape0_data = shape0.rasterize(0.1)\n", - "plt.plot(shape0_data[0], shape0_data[1],\"rx-\")" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[]" - ] - }, - "execution_count": 9, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "shape_r =copy.deepcopy(shape0)\n", - "shape_r_data = shape_r.rasterize(0.1)\n", - "plt.plot(shape_r_data[0], shape_r_data[1],\"x-\")\n", - "shape_r.apply_transformation(tr.rotation_matrix_z(np.pi/2)[0:2,0:2])\n", - "shape_r_data = shape_r.rasterize(0.1)\n", - "plt.plot(shape_r_data[0], shape_r_data[1],\"rx-\")" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[]" - ] - }, - "execution_count": 10, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "shape1 = copy.deepcopy(shape0)\n", - "shape1.reflect([1, 0])\n", - "shape1_data = shape1.rasterize(0.1)\n", - "plt.plot(shape0_data[0], shape0_data[1],\"x-\")\n", - "plt.plot(shape1_data[0], shape1_data[1],\"rx-\")" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[]" - ] - }, - "execution_count": 11, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAYIAAAD4CAYAAADhNOGaAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4xLjEsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy8QZhcZAAAgAElEQVR4nO3dd3hUVfrA8e+bhIQeCAmdUEIvoYWOoIKCFQFdxYarLuqKuLoqYNnlZ1l7R0FUrFgpgoBSVEAIvSWhJ6GFmhAIkJA2c35/ZIIBEkgyNzOZmffzPHmYOXNn7rnDmXnn3nPec8QYg1JKKd/l5+4KKKWUci8NBEop5eM0ECillI/TQKCUUj5OA4FSSvm4AHdXoDRCQ0NNkyZN3F0NpZTyKOvXr08xxoSdX+6RgaBJkyasW7fO3dVQSimPIiJ7CyvXS0NKKeXjNBAopZSP00CglFI+TgOBUkr5OA0ESinl4ywJBCIyWER2iEi8iIwr5PF7RCRZRDY5/u4v8NhIEdnl+BtpRX3ON3lpAtEJKeeURSekcM9naywpn7w0ocz3YWVdlfJknvJZc8X3i1WfZ6cDgYj4Ax8A1wBtgREi0raQTb83xnRy/H3ieG4I8F+gB9Ad+K+I1HS2TueLbBjM6G82nn0joxNSGP3NRvo0r2VJeWTD4DLfh5V1VcqTecpnzRXfL1Z9nsXZaahFpBcwwRgzyHF/PIAx5uUC29wDRBljRp/33BHA5caYBxz3PwKWGGO+vdg+o6KiTEnzCKITUhj15Xra1qtGzIE0rmlfl0Y1K7P/eAa/xB0mskGwU+WAZa9ldfm17euxZGcyE2/vTO+I0BK9b0qVR9EJKdz/xTra169erj5rZVUOed8vv8Yd4Z7eTfh+3f5SfZ5FZL0xJuqCcgsCwc3AYGPM/Y77dwE9Cn7pOwLBy0AysBN4zBizX0SeACoaY150bPcccMYY80Yh+xkFjAIIDw/vundvoXkRF3Xj+8uJOZDmeL2/ygu+Bc6UW/laVpePubI5j1/dCqW8QfKpLLq9tPjs/fL0WbO6/Jz7gKH0n+eiAoEVfQRSSNn50eVnoIkxJhJYDHxRgufmFRozxRgTZYyJCgu7IEP6kqITUkg6cYYxVzYnpEog0+7vwe6Xr2Pa/T0IqRLodLmVr2VleQV/IaRKBb5eve+Ca4xKeaoV8XlteWCbOuXms1YW5RNHdOaqtnUAaBpahWqVAhhzZXPrP8/GGKf+gF7AggL3xwPjL7K9P5DmuD0C+KjAYx8BIy61z65du5qSWBGfbDo/v9CsiE8+5/6UZfGWlK+ITy7zfZS2fMSUlab/a79fUD+lPNWK+GQTOWGBaTx2rpmz6UC5+axZWW63281Lc7eYJmPnmubj55nxMzebTv+3oNDvnpIA1pnCvpcLKyzJH3nzFSUCTYFAYDPQ7rxt6hW4PRRY5bgdAuwGajr+dgMhl9pnSQPBpCXxF7xhK+KTzcipqy0pn7Qkvsz3UdryJ3/cZHr+b/E5dVXKk01aEm+mrdpjGo+daxbEHTLGlI/PmlXle1PSzYgpK03jsXPNoLeXmv/N21rk90tJP89FBQKn+wgARORa4B3Hr/2pxpiXROR5x07niMjLwI1ALpAKPGSM2e547r3A046XeskY89ml9leazmJf9dxPccyLPcSG565yd1WUssz6vccZPimaL+7tTv+WJb9UXB7Z7IbPo/fwxoId+PsJ469tzYhu4fj5FXYFvXSK6iOwZPZRY8x8YP55Zf8pcHs8eZeMCnvuVGCqFfVQFwoK8CMrx+buaihlqazcvDYdFOAdObE7j5ziqekxbNp/gitb1+aloe2pF1zJZfv3yGmoVfEFBviRbbO7uxpKWSo7N69NB3p4IMjOtTNpSQIT/9hF1aAA3r2tEzd2rI+cPxyxjGkg8FKTlyYQ2TCYoAB/cmwGm92wevcxYpLSeLB/hLurp1Sp5LfrLEcgCArwIzohxSPb9eb9Jxg7I4bth09xQ8f6TLihLbWqBrmlLp4dTlWR8jMRD588A8CfO5M1s1h5vPx2HevIB9p28KTHtesz2Tb+N38bQz9cwfGMbD6+O4r3R3R2WxAACxLK3EE7i4snP/syI9tGjcoV+PCOLppZrDxedEIK//hiHenZNoIrVWDSnZ7TrlcmHGP8zBj2HMtgRPdwxl/bmuoVK7hs/2WZUKbKqd4RoWc/IDd2rO8xHxalLqZ3RCidw/OmJLs1qpFHtOuTmTk8PSuWER+vwgDf/KMHLw/r4NIgcDEaCLxYdEIKq3cfA+CnjQc0s1h5heiEFNbtTcVPYPqGpHLfrn/ffoSr31rGd2v28Y/LmvLro/3KXfDSQOCl8mcnHH9NawDu69v0nNkLlfJE+e26W5MQ6lavyMTbO5fbdn3sdBaPfreRez9fR3ClCsz8Zx+eua4tlQL93V21C2gg8FIxSWlMvL0zA9vkzVMSUiWQibd3JiYpzc01U6r08tu1nwih1YLoHRFa7tq1MYbZmw5w1dvLmB97iH8NbMHPj/SlU6Ma7q5akXT4qJfKH0qXa7MjAsmns7mrV2i5OyVVqiTy2/WLc7dRL7gicG5fmLsdSjvDs7Pi+G37UTo2qsFrwyNpVbeau6t1SRoIvFyAvx8hlQNJOZ3l7qooZZmU01l0aFB+hoza7Ybv1u7n5fnbyLHbefa6Nvy9T1P8LZweoixpIPABoVWDSD6lgUB5B7vdcCw9m9Bqge6uCgB7UtIZNzOGVYmp9GpWi1eGd6BxrSrurlaJaCDwYvlZmGHVgs6eEXhqFqZS+e25VZ1q2OyGsKpBbm3PNrth6vLdvLloBxX8/HhlWAdu7dbI5dNDWEE7i71YfhamMYbkU1m6brHyaPntefG2owCkZmS7rT1vP3ySYR+u4KX52+jbPJRFj/fntu7hHhkEQDOLvV50Qgr3fr6W7Fw7NSoFMvEOXbdYea7ohBQe+Go9pzJzqVYxgI/u6urS9pyVa+ODPxL48I94gitVYMKN7bg+sp7HBIAynYZalV+9I0Lp1awWf+xI5sZO9TQIKI/WOyKULuE1Wbozmb91bejS9rxx33HGzohh55HTDO3cgOeub0tIlfLRT+EsvTTk5fKyMI8DMGODZhcrzxadkMKqxGNU8BdmbTrokvackZ3LC3O3MmxSNKcyc5l6TxRv39rJa4IAWBQIRGSwiOwQkXgRGVfI44+LyFYRiRGR30SkcYHHbCKyyfE3x4r6qDz5fQKvDu8AwNDODcptFqZSl5LfnlvUrkrbetVdklUcHZ/C4Hf+5NPlu7mjRzgLH+vHla3rlNn+3MXpQCAi/sAHwDVAW2CEiLQ9b7ONQJQxJhKYDrxW4LEzxphOjr8bna2P+kt+FuY17etRrWIAxlDusjCVKq789pyank2zsKplmlWcdiaHcTNiuP2T1fgJfDeqJy/e1IFq5WSSOKtZ0UfQHYg3xiQCiMh3wBBga/4Gxpg/Cmy/CrjTgv2qSyg4pK5ZWFUSU06XqyxMpUriwf4RZGTncjAtk4iwvHH6ZdGeF209wrM/xZJ8KosH+jfjsYEtqVih/M0PZCUrAkEDYH+B+0lAj4tsfx/wS4H7FUVkHXkL279ijPmpsCeJyChgFEB4eLhTFfZFEWFViI4/5u5qKOWUxOR0IO+HjdVSTmcxYc4W5sYconXdanx8dxSRDcvv/EBWsiIQFDZuqtAxqSJyJxAF9C9QHG6MOSgizYDfRSTWGJNwwQsaMwWYAnnDR52vtu+YvDSBCn5+HD6ZyemsXKoGBWhimfIo+clk+RnyEWFVLWvDxhh+2nSA//t5KxlZNv59VUsevDyCCv6+M5bGiiNNAhoVuN8QOHj+RiIyEHgGuNEYc3a+A2PMQce/icASoLMFdVIFRDYMZl7sIQB2J6drYpnyOPnJZEt3JCOSN7mbFW344Ikz3Pv5Wh77fjNNQ6swb0xfHhnQwqeCAFiQUCYiAcBOYABwAFgL3G6M2VJgm87kdRIPNsbsKlBeE8gwxmSJSCiwEhhijNnKRWhCWcn9uG4/T06P4eq2dVi39zgTb9fEMuVZohNSuGfqWipW8CPA38+pNmy3G6at2cerv2zHZjc8OagVI3s38ZhJ4kqrzJaqNMbkAqOBBcA24AdjzBYReV5E8kcBvQ5UBX48b5hoG2CdiGwG/iCvj+CiQUCVzk2dG+AvsHDrEe7sEa5BQHmc3hGhVAr042RmrlNteHdKOrd9vIrnfoqjU6MaLHysH/f29ZyZQsuCJZnFxpj5wPzzyv5T4PbAIp4XDXSwog7q4tbuSUVEqFstkK9X76NnRC0NBsqj/Bp3iLQzufSOqFWqNpxrs/PJ8t28vWgngQF+vDY8kluiGnrM9BBlybcuhPmo/D6Bwe3rkpqRw9u3dtTEMuVRohNSeOLHGAAeHdCixMlkWw+e5KYPV/DKL9vp3zKMxY/3528eOlNoWdBA4APyE3Guj6xHdq6d6hUraGKZ8igxSWkMaFMbfz8hsmGNYieTZeXaeHPhDm6cuJzDaZl8eEcXPrqrK3WqV3RRzT2DTjrnA/KH1x05mQnAxn0nuLdvU700pDzGg/0juP3jVbSpV+3s4u+XSiZbvzdvkrj4o6cZ1qUBz13XlppeND+QlTQQ+JA61StSP7giG/Yd516aurs6ShWbzW7YvP8Ew7o0vOS26Vm5vLFwB59H76F+cCU+/3s3Lm9V2wW19FwaCHzI5KUJNKpVmY37Tpwt08QyVZ7lJ5LVrBxIeraNLo1rXLTN/rkrmfEzY0k6foa7ezXmqcGtqRqkX3OXon0EPiSyYTCxSWkcOHGGoyczNbFMlXv5iWTT1ztmsTEU2mbTMnJ4avpm7vp0DYH+fvzwQC+eH9Jeg0Ax6QplPuaLlXv47+wtXNuhLqsSUzWxTJV70Qkp3PPZWvwEKgcGXNBmf407zHOz40hNz+aBfs0YM6CF108SV1plllCmPMtt3RoR4CfMjz2siWXKI/RqVotAfyEzx35Omz16KpN/TlvPg1+vJ6xqELMf7sNTg1trECgFPW/yMev3HkcEqlUM0MQy5RF+WLef01k2rmxdO6/NNqvFobRMnp+7lTM5Np4c1IpR/Zr53PxAVtJA4EPy+wTu7tWET5fv5vWbIxn9zUa9PKTKreiEFCbMyZt15uVhHVideIyRn60hx2bo2rgmrw6PpHlt66ek9jUaQn1IfmLZfX3zho4ez8jWxDJVrsUkpdE4pDKt61ZjwZbDjJ8Zi58IV7Wpw48P9NIgYBHtLPZRg99ZRo3KFfhuVC93V0WpIqVl5NDx+YVn71/WIpT/De1Ao5DKbqyV5yqqs1gvDfmoK1rX5uNliZzMzKG6l67Dqjxbjs3OrVNWnr3/xi0dGd6lgc4PVAb00pAPmrw0gdrVgsi1G5bvypu0KzohhclLL1gYTimXm7w0gW9W7+WmD1aw/fApAD64vTMpp7M0CJQRDQQ+KLJhMO/9tosqgf78vv2oJpapciMzx8a2gyd5elYc+1MzAOgTUYvnZm/R9lmGNBD4oN4RoXxwRxdybIZ5MQcZPU1HDin3W7cnlWvf+5PZmw/Sv2Uodkf35aakE9o+y5hlgUBEBovIDhGJF5FxhTweJCLfOx5fLSJNCjw23lG+Q0QGWVUnVbTeEaEMaFObMzl2Lmt58VkclSpLp7Ny+e/sOG75aCVZOXa+vLc7X9zbg2ahVQC4o0djbZ9lzJJAICL+wAfANUBbYISItD1vs/uA48aY5sDbwKuO57YFbgPaAYOBDx2vp8pQdEIKqxKPEegv/BJ7WBepUW6xdGcyg95exper9jKyVxMWPtaPfi3DWLYzmdgDabSoXZXp65O0fZYxq84IugPxxphEY0w28B0w5LxthgBfOG5PBwZIXs/PEOA7Y0yWMWY3EO94PVVG8vsEPrijC7dENcJgeHjaBv2wKZc5kZHN4z9sYuTUNVSs4Mf0B3sx4cZ2VAkKIDohhX9O24ABnr6uTYlXI1MlZ1UgaADsL3A/yVFW6DaOBe/TgFrFfC4iMkpE1onIuuTkZIuq7ZvyE8t6R4QyrEtDcmyGW6IaaWKZcon5sYcY+NZS5mw6yOgrmjNvzGV0bRxy9vGYpDRa161GWLUgLmseWuzVyFTpWZVHUNiYrvMz1YrapjjPxRgzBZgCeQllJa2g+kvBedy7hNegaWgVNu8/wfcPaHKZKjtHT2byn9lb+HXLYdo3qM4X93anXf0LRwL9LaoRby7cwT29mxDgmD/oUquRKedYdUaQBDQqcL8hcLCobUQkAAgGUov5XFVGPlqWSFTjmqzenXp2uJ7mFCgrGWP4Yd1+Br61lN93HGXs4Nb89M8+5wSByUsTzl76+XnzQXJshoiwqtoOXcSqQLAWaCEiTUUkkLzO3znnbTMHGOm4fTPwu8mb32IOcJtjVFFToAWwxqJ6qUuIbBjMoq1HAJi18YDmFChL7U/N4O6pa3hqegyt61bn10cv46HLI87+0s+XvwBNdEIKMzYk0TikMq8t2KHt0EUsm2tIRK4F3gH8ganGmJdE5HlgnTFmjohUBL4COpN3JnCbMSbR8dxngHuBXOBfxphfLrYvnWvIWtEJKdz96RqqBPnj7+enY7aV02x2w5cr9/D6gh0IMO6a1tzRozF+fkVnBkcnpPDQ1xtIO5ND5UB/PhkZpe3QYmU+15AxZj4w/7yy/xS4nQncUsRzXwJesqouqmR6R4RyRevaLNp6hBs71tcPn3JK/NFTPDU9hg37TnB5qzBeGtqBBjUqXfJ5vSNCCQ+pROyBHEZ010WTXEkzixXRCSms25NKlUB/5sce0mF6qlRybHYm/r6La99dTmJKOm/f2pHP7ulWrCAA8GvcIWIPnKRtvepnL1Mq19BA4OMK5hSMGdCCXLvhwa/W64dQlUhsUho3vL+cNxbu5Kp2dVj8eH+Gdm5Y7EniohNSeOz7zQC8N6KT5g64mAYCH1cwp2BEj3CqBgXQrkGwjtlWxZKZY+OVX7Zz04crSE3P5qO7uvLB7V0IrRpUotdZv+c4/n4wsE1tmteuprkDLqbrEfi4gjkF1StW4PYe4Xy6fDevDY90Y62UJ1ideIxxM2PZnZLOrVGNePq6NgRXKt3aFjUqV+B0lo1R/f5qj5o74Dp6RqDOEeAnGGP4dPnus2WaV6AKOpWZw7M/xXLrlFXk2u1Mu78Hr94cWeIgkJ87YLMbPlm+m46NapBrs2tbcwMNBOocfVuEEuDvxzer93EiI1vzCtQ5/th+lEFvL2Pa6n3c17cpC/7Vjz7NS/erPT934L3fdrH3WAaXtwxj9Lfa1txB1yxWF/huzT7GzYylR9MQdh09rXkFitT0bF6Yu5VZGw/QonZVXr05ki7hNZ1+3ej4FO6auoaqQf74ix8T79C2VpZ0zWJVbLd1D+ejZYms3p3K/X2b6gfThxljmBd7iP/O3kLamRzGDGjBw1dEEBRgzUzxxzNysNkNaWdyGXNlc21rbqKXhtQFohNSSE3PRoCvVu3VIXw+6sjJTEZ9tZ7R32ykQc1K/PxIXx6/qqVlQSAr18aEn+PwF2H0Fc35evU+bWtuooFAnSO/T2DSnV24q1djcmx2Hvpa1yrwJcYYvluzj4FvLWXZzmSevrY1Mx/qTZt61S3dzws/byX5VDZPDm7FE4Naae6AG+mlIXWOgnkFrepUY9aGAzQLrUJMUpqetvuAfccyGDczhuiEY/RoGsKrwyNp4lgy0krH07P5cX0SkQ2Dzw5hLpg7oG3NtfSMQJ3jwf4RZz+EtaoG0bVxTTbuP0Fkg79GcuhwUu9jsxs++TORq99ZSkxSGi8Nbc+3/+hpaRAoONX0e7/vIsdm544ejc9pS70jQs/JbVGuoYFAXdQ9fZrgJzB+Ziw2u9HhpF5o55FTDJ8UzYvzttE7IpRFj/e75EyhpZE/XHTmhiS+WrmXy1uF8eqv27UtlQM6fFRd0hsLtjPxjwQGtKnNxn0ndDipl8jOtTNpSQIT/9hFtYoV+O8NbbmxY/1izw9UGtEJKYycugYMVKkYwId3dNG25EI6fFSV2r+vbsWMDQf4bdtR7uvTRD+4XmDz/hOMnRHD9sOnuLFjff57Q1tqlXB+oNI4np5Dji3vx+fdPRtrWyonnLo0JCIhIrJIRHY5/r0gw0REOonIShHZIiIxInJrgcc+F5HdIrLJ8dfJmfqosrEy8RgZ2Tb8/eDLVXuJjtdRHZ7qTLaN/83fxtAPV3AiI4dP7o7ivRGdXRIEkk9lMXbGZvz9hNFXROhw0XLE2T6CccBvxpgWwG+O++fLAO42xrQDBgPviEiNAo8/aYzp5Pjb5GR9lMUKDid99rq25NgM//hynX6APdDKhGMMfncZU5Ylcmu3cBY+3o+Bbeu4ZN/GGB74aj2ns2y8OqwDTwxqrcNFyxFnA8EQ4AvH7S+Am87fwBiz0xizy3H7IHAUCHNyv8pFCg4nHdmrCT2bhWAzhj93Jbu7aqqYTmbmMH5mLCM+XgXAN//owcvDOlC9YulmCi2NmRsOsGHfce7sEc7NUY0AdKrpcsTZQFDHGHMIwPFv7YttLCLdgUCg4NjDlxyXjN4WkSLPT0VklIisE5F1ycn6JeQqBYeT+vkJnRrVAAOb96dht+dd69XhpOXXb9uOcPVby/h+7T5G9WvGr4/2c8l1+YJDRQ+eOMOEn7fQuk416tc8d7UyHS5aPlwyEIjIYhGJK+RvSEl2JCL1yFu8/u/GGLujeDzQGugGhABji3q+MWaKMSbKGBMVFqYnFO7Sr2UY/n5CdMKxs9NP6HDS8ufY6SzGfLuR+75YR3ClCsz8Zx+evrYNlQKtmR7iUvKHikbHpzB2RgzZuXYOnczM+yGhyh2nho+KyA7gcmPMIccX/RJjTKtCtqsOLAFeNsb8WMRrXQ48YYy5/lL71eGj7hUdn8LIz9ZgN4YqgQFMvqurjv4oJ4wxzNl8kP/7eSunMnMYfUULHro8gsAA16cMRSekcN8X6ziTbaNyoD+fjIzSduJmRQ0fdbZ1zAFGOm6PBGYXsuNAYBbw5flBwBE8kLyByzcBcU7WR7lA7+ah3N2rCTY72I2hee2q7q6SAg6lneH+L9bx6HebaBRSmbmPXMajA1u4JQhA3jKWZ7JtANyns9iWa862kFeAq0RkF3CV4z4iEiUinzi2+RvQD7inkGGi00QkFogFQoEXnayPcoHohBRmbTzAbd0bcTrLxogpq8jMsbm7Wj7LbjdMW72Xq95axoqEFJ69rg0zH+pNq7rV3FannUdO8c9pG/D3Ex7qH8E0HSparmlmsSqR/D6B/JFE7y7eyduLd9GufnXmPtL3bFZqdEIKMUlp2hFYxvakpDNuZgyrElPpHVGLl4d1oHEt6yeJu5TJSxOIbBhM74hQUtOzGfTOUpJPZTOkU33eva3zBe1GuUdZXRpSPqbgcFKARwe25PKWoWw5eJJxM2MBtAPZBXJtdqYsS2DQO8vYcuAkrwzrwLT7e7glCMBfncPLdibz4NfrOXY6m8qB/tzaTYeKegI9I1BOM8ZwxyeriU44xnUd6rEy8Zj+8itD2w+fZOz0GDYnpTGwTR1evKk9dYMrurtaRMen8PfP15KVa6dKkD8f362dw+WNnhGoMiMiTL2nG3WqBzEv9hB9m4fqF0AZyMq18dainVz/3nKSjp/h/RGd+fjuruUiCBhj+H37UbJy80aG39dHO4c9iU46pyyxYd9xsnLthFUNZM7mg7SoU5VHrmzh7mp5jY37jjN2Rgw7j5xmaOcGPHd9W0KqBLq7WkBeZ/WEn7fw5cq9BAX4MapfM75evY+eEbU0GHgIDQTKafl9Ah/e0YV29YIZNmkFby7cye7kdN66tdM522kHcslkZOfy5sKdTF2xm7rVKzL1niiubO2a+YEKU7BTGPKCwH1frOWPHclUrODH1JHd6N08lF4RtbRz2IPopSHltIIdyMGVK/DTw31oWLMiMzce4I0F2wHtQC6NFfEpDHpnGZ8u380dPcJZ+Fg/twYBKJAxnJCCzW4Y+dka/tiRTLt61c8GAdDOYU+jncWqTKRn5XLz5Gi2HTqlC9qUUNqZHF6ev43v1u6naWgVXhnWgR7Narm7WmdFJ6Tw8LQNhFULYueR09zctSFv3NLR3dVSxaAL0yiXqhIUwMyH+nD1O0v5bdtROjWqQbcmIe6uVrm3cMthnv0pjpTTWTzQvxmPDWxJxQqumR+ouJqGVqFiBX92HjlN74haGgS8gF4aUmVm4/7jnM7MpWPDYDbtP8F17/3J4bRMd1erXEo5ncXobzYw6qv1hFQJ5KeH+zD+mjblLggs35XCoLeXcSgtk0Ht6rL98CnNGPYCGghUmcjvE/jgji7MHt2X0Vc2Z+eR0/R//Q+mLEu4YFtfncbaGMOsjUkMfGspC7cc4d9XteTnR/oS2dD9s3QWnErabje8/9su7vx0Nacyc3nj5kg+uqurLi7jJTQQqDJxfgbyE1e34o2bIwkK8ON/87fz1PTN2O3GpzuRD5w4w98/X8tj32+maWgV5o3pyyMDWlDBv3x8LPM7hhdtOcx9X6zlzUU78fcTnhjUUheX8TLaWaxcKj0r17HU5TFqVwsiM8fmc9NY2+2GaWv28cr8bdgNPDW4FXf3aoK/n7i7auew2Q0v/7KNT//cjQhUrODPJ3dHnR0ZpDyPZharcqFKUADT7u/BoHZ1OHoqi5OZubw0dyvzYg6es523Xi5KTD7NbVNW8dxPcXQOr8nCx/rx9z5N3RoECl4Cyjd1+W56v/wbn/y5m3rBFbEbuL9vUw0CXkoDgXK5lYnHWLvnOA/2b0bFCn5sO3yKh7/ZyNjpMWTm2LzyclGuzc7kpQlc8+6fbD98ktdujuSr+7rTKKSyu6t2Tm7AwRNnGPHxKp6fu5Vcu51HrmxOZq6dMVc252udStpr6fBR5VLnT0fcr2UYD329gTrVg/h+3X7mxR7EZoc3/xZ59rpzwctGnpidvPXgSZ6asZm4AycZ1K4OLwxpT+3q7pkf6PzM4Hzdm9Tk75+tJddux2aHoZ0bMKRjfR7/cfPZ/6uemi3stbSPQLlUYV9E+V/uOw6fYtbGAwAE+AldG9dk66GTfHRnV3o3D/W4Oe2zcui9B1UAABvKSURBVG1M/D2eSUsSqFG5As8Pac817eueXbPBHQq+h50b1eTtxTv5dPlubHaDvx/Y7HBP78ZMuLH9Rf+vPCkQq78U1UfgdCAQkRDge6AJsAf4mzHmeCHb2chbiQxgnzHmRkd5U+A78hav3wDcZYzJvtg+NRB4n/wvqDt7hPPFyr30iajFioRjpJ3JwU+gW5MQth46yfsjOrP98KlCv6CmLEtkVL9m5eKLa/3eVJ6aHkNCcjrDujTguevaUrMMJokr6su6qPdiVWIqAX7CB3/EY4wh22ZoUKMiA9vUYc7mg9zVszFfr97nMcFWlUxZBoLXgFRjzCsiMg6oaYwZW8h2p40xFyxuKyI/ADONMd+JyGRgszFm0sX2qYHAu5z/Sz///lt/60hqejavL9jBIUciWqC/H01DK7M3NYNHB7Tgps4N2HnkFI99v5mHLm/GpCWJF7yOK7/U0rNyeX3BDr5YuYf6wZV4aWh7Lm9Vu8z2V9R791D/Zny4JIHnrm+Lv58wZ9NBluw4is3xcfcXsBkY3qUBw7s0ZPS3F76GBgPvU5aBYAdwuTHmkGMx+iXGmFaFbHdBIHAsWp8M1DXG5IpIL2CCMWbQxfapgcC7XOwSRH5H5s1dGvDNmv1c3iqM/cfPEJt0AnuBplsxwI+w6kEE+vmx73gG17Svx/L4FJd+mf25K5nxM2NJOn6Gkb0a8+Tg1lQNKvtuuOiEFO77fB21qgZyKC2T6hUDSM+ykW2zn7Ndx0bBXN22LlUC/Xn3t11nf/0PaleHGzrWLxdnUqpsleVcQ3WMMYcAHMGgqJ8/FUVkHZALvGKM+QmoBZwwxuQ6tkkCGhT2ZBEZBYwCCA8Pt6Daqrwo7Msm/0up4C/Ty1vXPnu/Y8MaPDMrjp82HaBnsxDa1gsmNT2L1Iwc9hxLZ87mg4y5srlLgkBaRg4vztvKj+uTaBZWhR8f7OXSeZV6R4SSnWsj6fgZmoZWJqpxCCFVAwmpHMjaPaks3naU0Vc054lBrc7J+C7YAXxDx/oXvKaeDfiOYgUCEVkM1C3koWdKsK9wY8xBEWkG/C4iscDJQrYr9BTFGDMFmAJ5ZwQl2K/yUOdnJ5+fxbpsV/LZYY1jBrQ4e1lj+a5kmteu6pLFUX6NO8xzs+NITc/mn5dHMGZAC5fPDxSdkILNQJfwGuw5lsHQLg3OvhcfLUs8+x71bl7rou+pfvH7rmIFAmPMwKIeE5EjIlKvwKWho0W8xkHHv4kisgToDMwAaohIgOOsoCFwsLDnK99T3DOF/F+1+X0EdYMr0qpONe7oGV5m17qPnspkwpwtzI89TNt61fnsnm60b+D6vIf8X/gV/IRuTUN4YlCrc96L4gz91F//yoqEsjnASMftkcDs8zcQkZoiEuS4HQr0AbaavA6KP4CbL/Z8pQoq6lftivhjTLy9M5UDA84pt3IeHGMM09cncdVby1i87ShPDmrF7NF93BIE4K/3ws+RmXz+e1HU2ZRSBVnRR/AK8IOI3AfsA24BEJEo4EFjzP1AG+AjEbGTF3xeMcZsdTx/LPCdiLwIbAQ+taBOyosVdaZQ2K9aK3/tJh3P4OlZcSzbmUxU45q8MjyS5rUvGAjnUu56L5R3cToQGGOOAQMKKV8H3O+4HQ10KOL5iUB3Z+uhVFmx2w1frdrLq7/mLbv5fze2466ejc/+ClfK0+kUE0pdRELyacZOj2Hd3uP0axnG/4a2p2FN988PpJSVNBAor5Gfj1BQacfD59jsTFmWyLu/7aJSBX/euKUjw7s0cOv0EIWx8piV79LZR5XXyE8+y8jOS0sp7SymcQfSGDJxBa8v2MHANrVZ9Hg/bu7asNwFAfjrmO2O7DpvnLlVlT2ddE55leiEFO78ZDXNwqqSmp5doqGjmTk23v1tF1OWJVKzciAv3tSOwe3rlXGNnRedkMLtH68+m0egU0OoopRlZrFS5UbviFCCK1Ug/ujpEmUWr92TytjpMSSmpHNL14Y8e11bgitXKOPaWqN3RCj+Ahv2nXBZNrXyLnppSHmV6IQU0s7knM0svtRCKqezcvnP7DhumbySbJudr+7rzuu3dPSYIADnZhbr4jGqNDQQKK+Rf308P7N44u2dz668VZilO5MZ9PYyvlq1l3t6N2HBv/pxWYswF9faOednFl/qmJUqjAYC5TXys2wvlVl8IiObx3/YxMipa6hYwY/pD/Ziwo3tqOKCmUKtVlRmsWYQq5LwvJavVBEulWVrjOGXuMP8Z3YcJzJyGH1Fc0Zf2dzlk8RZqSSZxUoVRQOB8glHT2by3Ow4Fmw5QvsG1fni3u60q69DLJUCDQTKyxlj+HF9Ei/O3UpWrp1x17Tm/r5NCfDXq6JK5dNAoLzG+Vm2+1MzeOjr9cQdPEn3JiG8MrwDzcLcO0mc1TSzWFlBA4HyGvlZtkH+fsyLPcSirUfIttn5e58mPHddW6+cJK6ozOKJt3d2c82UJ9HzY+U1ekeE8vQ1rTl0Mm+he4PhvRGd+e8N7bwyCMBfo4Ry7Ia1u1N10XlVKnpGoLxCjs3O5CUJvP97/NmyB/tHcON5a/F6I80sVs7SMwLl8WKT0rjh/eW8uWgnXRrXQIDmtasyzUeybDWzWDnLqUAgIiEiskhEdjn+rVnINleIyKYCf5kicpPjsc9FZHeBxzo5Ux/lWzJzbLz8yzaGfLCc1PRsHr+qJTuPnKZejeJlFnsDzSxWVnD2jGAc8JsxpgXwm+P+OYwxfxhjOhljOgFXAhnAwgKbPJn/uDFmk5P1UT5ideIxrnn3Tz5amsjfohqx6PH+BAb4lfmaxeWNZhYrKzjbRzAEuNxx+wtgCXlrEBflZuAXY0yGk/tVPupUZg6v/rqdr1fto1FIJabd34M+zfOuiftilq0vHrOynrOBoI4x5hCAMeaQiNS+xPa3AW+dV/aSiPwHxxmFMSarsCeKyChgFEB4eLhztVYe6Y/tR3lmViyHTmZyX9+m/Pvqlmd//SulSu+SnyIRWQzULeShZ0qyIxGpR94C9gsKFI8HDgOBwBTyziaeL+z5xpgpjm2IioryvNV0VKmlpmfzwtytzNp4gBa1qzLjod50Cb+gO0opVUqXDATGmIFFPSYiR0SknuNsoB5w9CIv9TdgljEmp8BrH3LczBKRz4Anillv5QOMMcyNOcSEOVtIO5PDmAEtePiKCIICCp8kzhezbH3xmJX1nO0sngOMdNweCcy+yLYjgG8LFjiCB5K3GOxNQJyT9VFe4sjJTP7x5Xoe+XYjDWpWYu6Yvjx+VcsigwBYt2axJ9E1i5UVnFqzWERqAT8A4cA+4BZjTKqIRAEPGmPud2zXBFgBNDLG2As8/3cgDBBgk+M5py+1X12z2HsZY/h+7X5emr+N7Fw7T1zdir/3aVLsSeKcWbPYU+maxaq4ymTNYmPMMWBAIeXrgPsL3N8DNChkuyud2b/yLvuOZTBuZgzRCcfo0TSEV4dH0iS0Soleo7RrFnsyzSxWztIhF8rtbHbDZyt288bCHQT4+fG/oR24rVujUs0PdP6axT0jann9F+P5mcW+cMzKWjrFhHKrHYdPMXxSNC/O20bviFAWPd6P23uElzoIlGTNYm+gmcXKChoIlFtk59p5Z/FOrn//T/alZvDubZ34dGQU9YIrlfo1i7tmsTfRzGJlBb00pFxu8/4TPDU9hh1HTjGkU33+c31balUNcvp1fTHL1hePWVlPA4FymTPZNt5atINPl++mdrWKfHJ3FAPb1nF3tZTyeRoIlEusTDjGuJkx7D2Wwe09whl3TWuqV6zg7moppdBAoMrYycwcXp6/nW/X7KNxrcp8848eZXbZwhezbH3xmJX1tLNYlZnFW49w9VvL+H7tPkb1a8avj/Yr02vXmlnsG8esrOdUZrG7aGZx+XbsdBb/9/NW5mw+SOu61Xh1eCQdG9Vwyb41s1gzi1XRyiSzWKmCjDHM2XyQCXO2cDorl8cGtuShyyMIDHDdiadmFvvGMStraSBQljiUdoZnZ8Xx2/ajdGpUg9dujqRlnWour4dmFvvGMStraR+Bcordbpi2ei9XvbWMFQkpPHtdG2Y81NttQUAzi73/mJX1NBCoUtuTks7tn6zimVlxRDYMZuG/+nP/Zc3wL8X0EFbQzGLfOGZlPb00pEos12Zn6ordvLlwJ4H+frwyrAO3dmtE3rIS7uOLWba+eMzKehoIVIlsO3SSsTNiiElKY2CbOrx4U3vqBld0d7WUUk5w+tKQiNwiIltExO5YkKao7QaLyA4RiReRcQXKm4rIahHZJSLfi0igs3VS1svKtfHWop3c8P5yDhw/w8TbO/Px3V3LVRCYvDThgmvj0QkpTF6a4KYalT1fPGZlPSv6COKAYcCyojYQEX/gA+AaoC0wQkTaOh5+FXjbGNMCOA7cZ0GdlIU27DvO9e8t573fdnFDx/osfrw/10fWd/uloPNpQplvHLOyntOBwBizzRiz4xKbdQfijTGJxphs4DtgiGOt4iuB6Y7tviBv7WJVDmRk5/LC3K0MnxTN6axcPrunG2/f2omaVcrnSVt+R+nhtEx2HDnF6G82en1yVf4x59gNa3en+sQxK+u5qo+gAbC/wP0koAdQCzhhjMktUH7BkpYAIjIKGAUQHh5edjVVAKyIT2HczBj2p57hzp7hjB3cmmoeMEmcJpT5xjEraxXrjEBEFotIXCF/Q4q5n8KuIZiLlF9YaMwUY0yUMSYqLCysmLtVJZV2JodxM2K445PVBPj58f2onrx4UwePCAJwYUKZL4ynPz+hzBeOWVmrWGcExpiBTu4nCWhU4H5D4CCQAtQQkQDHWUF+uXKDhVsO8+xPcRxLz+bB/hH8a2ALKlbwd3e1iu38hLI7eoZ7/aWS8xPKnhjUyuuPWVnPVQlla4EWjhFCgcBtwByTN+PdH8DNju1GArNdVCflkHwqi4e/2cCor9ZTq2oQP/2zD+Ouae1RQQA0oQx845iV9ZzuIxCRocD7QBgwT0Q2GWMGiUh94BNjzLXGmFwRGQ0sAPyBqcaYLY6XGAt8JyIvAhuBT52tkyoeYww/bTrA//28lYwsG09c3ZIH+kdQwd8zE859MbnKF49ZWc/pQGCMmQXMKqT8IHBtgfvzgfmFbJdI3qgi5UIHTpzhmVmxLNmRTJfwvEnimtd2/fxASin308xiH2O3G6at2ccr87dhN/DfG9pyd68mbpsfSCnlfhoIfEhi8mnGzYhlzZ5U+jYP5eVhHWgUUtnd1bKMLy7b6IvHrKznmReDVYnk2uxMWpLA4Hf/ZPvhk7x2cyRf3dfdq4IAaGYx+MYxK+vpUpVebuvBkzw1YzNxB04yqF0dXhjSntrVy8/8QFbTpSp1qUpVNF2q0sdk5tiY+Hs8k5cmUKNyIJPu6MI1Heq5u1plTjOLfeOYlbU0EHih9XtTeWp6DAnJ6Qzv0pDnrm9Djcrlc34gq+lSlb5xzMpa2kfgRdKzcpkwZws3T15JZo6dL+7tzpt/6+hTQUCXqvT+Y1bW00DgJZbtTObqt5fxefQe7u7ZmAWP9aN/S9+ak0kzi33jmJX19NKQh0vLyOGFeVuZvj6JZmFV+PHBXnRrEuLuarmFL2bZ+uIxK+tpIPBgv8Yd4rnZW0hNz+afl0cwZoBnTRKnlCofNBB4oKOnMvnv7C38EneYtvWq89k93WjfQMeNK6VKRwOBBzHGMGPDAV6Yu5UzOTaeHNSKUf2aeewkcVbzxSxbXzxmZT39BvEQ+1MzuHvqGp74cTMtaldl/pjLePiK5hoECtDMYt84ZmU9zSwu5+x2w5cr9/DagrxloccObs1dPRufHSWizqWZxZpZrIqmmcUeKP7oacbNiGHd3uP0axnG/4a2p2FN75ofyGqaWewbx6yspYGgHMqx2ZmyLJF3F++iUqA/b97SkWFdGiCiZwGXopnFvnHMylpOXWAWkVtEZIuI2EXkgtMNxzaNROQPEdnm2PbRAo9NEJEDIrLJ8XdtYa/hS+IOpDFk4gpeX7CDgW1rs/jx/gzv2lCDQDFoZrFmFqvScbanMQ4YBiy7yDa5wL+NMW2AnsDDItK2wONvG2M6Of4uWMHMV2Tm2Hj11+0M+WAFyaezmHxnFz68oyth1YLcXTWPoZnFvnHMynpOXRoyxmwDLvpr1RhzCDjkuH1KRLYBDYCtzuzbm6zdk8rY6TEkpqRzS9eGPHtdW4IrV3B3tTyOL2bZ+uIxK+u5tI9ARJoAnYHVBYpHi8jdwDryzhyOF/HcUcAogPDw8LKtqIuczsrltV+38+XKvTSsWYmv7uvOZS18a34gpZT7XTIQiMhioG4hDz1jjJld3B2JSFVgBvAvY8xJR/Ek4AXAOP59E7i3sOcbY6YAUyBv+Ghx91teLdlxlGdmxXEw7Qx/79OEJ65uRZUg7btXSrneJb95jDEDnd2JiFQgLwhMM8bMLPDaRwps8zEw19l9lXfH07N5Yd5WZm44QPPaVZn+YG+6Nq7p7mp5BV/MsvXFY1bWK/O0VMnrQPgU2GaMeeu8xwoumTWUvM5nr2SMYX7sIa56eylzNh3kkSubM29MXw0CFtLMYt84ZmU9pzKLRWQo8D4QBpwANhljBolIfeATY8y1ItIX+BOIBeyOpz5tjJkvIl8Bnci7NLQHeMDRuXxRnpZZfPRkJs/NjmPBliN0aBDMq8MjaVu/urur5ZU0s1gzi1XRyiSz2BgzC5hVSPlB4FrH7eVAocOKjDF3ObP/8s4Yw4/rknhx3laycu2Mu6Y19/dtSoDOD1RmNLPYN45ZWUt7J8vI/tQMxs+MZXl8Ct2bhPDK8A40C6vq7mp5Pc0s9o1jVtbSn6YWs9kNU5fv5uq3l7Fp/wleuKk9343qqUHABTSzWDOLVeloILDQriOnuGVyNM/P3UqPZiEsfKyfzhTqQppZ7BvHrKynl4YskGOzM3lJAu//Hk+VIH/eubUTQzrV1/mBXMwXs2x98ZiV9TQQOCk2KY0np29m++FTXB9Zjwk3tiO0qs4PpJTyHBoISikzx8bbi3fy8bJEQqsGMeWurlzdrrAEbKWUKt80EJTCqsRjjJsRw55jGdzWrRHjr21DcCWdJM7dfDHL1hePWVlPO4tL4FRmDs/MiuW2KauwG5h2fw9eGR6pQaCc0Mxi3zhmZT1ds7iY/th+lKdnxXLkZCb39mnK41e3PDs6RZUfmlmsmcWqaLpmcSmlpmfz/M9b+GnTQVrUrsqHD/Wmc7jOD1ReaWaxbxyzspYGgiIYY5gbc4gJc7aQdiaHRwe04J9XRBAU4O/uqqmL0Mxi3zhmZS3tIyjE4bRM/vHleh75diMNalZi7pi+PHZVSw0C5ZxmFmtmsSodDQQFGGP4ds0+rnprKX/uSuaZa9sw86HetK6rM4V6As0s9o1jVtbTS0MOe4+lM25GLCsTj9GzWQivDIukSWgVd1dLlYAvZtn64jEr6/l8ILDZDZ+t2M0bC3dQwc+P/w3twG3dGun8QEopn+HTgWDH4VM8NSOGzftPMKB1bV4c2p56wZXcXS2llHIpp/oIROQWEdkiInYRuWBsaoHt9ohIrIhsEpF1BcpDRGSRiOxy/Fsm4zInL004p/MsO9fOv3/YxOB3lpGYfJp3b+vEJyOjqBdcieiEFO75bM0FnW0XK5+8NOGCfVzqOb5W7s73aPLSBLyVt7Y7/UwV772wqm0721kcBwwDlhVj2yuMMZ3OS2YYB/xmjGkB/Oa4b7n87MvohBQ27T/BgDeXMGPDAdrWr46/nxBWLQgROTsCo0/zWueMvLhUeWTD4HP2UZzn+Fq5K98jzSwuf///5bm9lPfyi70XVrVtSzKLRWQJ8IQxptB0XxHZA0QZY1LOK98BXG6MOeRYyH6JMabVpfZXmszi6IQURn25ntNZeV8SAX5C09AqZGTncjgtk+BKFUg7k0Pd4IpUDgwocTlg2Wt5a7mr3qMDJzJpXtv3MouDAvzItdnd/v/sae3FE8oLvhc3dWnAH9uTS9W23Z1ZbICFImKAj4wxUxzldfIXq3cEg9pFvYCIjAJGAYSHh5e4Ar0jQhncvg7T1x+gee0qtKxT7exjlQJPEX803elyK1/LW8tdt2/fyizu2SyEVYmp5eb/2fPaS/kvz39sxvoDlrftS14aEpHFIhJXyN+QEuynjzGmC3AN8LCI9CtpRY0xU4wxUcaYqLCwsJI+neiEFH7fnsyYK5uTmp7DnT0b8+EdXbmzZ2NS03OcLrfytby13NX7/nr1vguuq3qj6IQUdh45XW7+nz21vZTn8jJv28YYp/+AJeRd+inOthPIu4wEsAOo57hdD9hRnNfo2rWrKYkV8cmm8/MLzYr45HPuT1kWb0n5ivjkMt+Hp5e78z3Kv++NvLXd6WeqeO9FSds2sM4U9r1cWGFJ/y4WCIAqQLUCt6OBwY77rwPjHLfHAa8VZ38lDQSTlsRf8IatiE82I6eutqR80pL4Mt+Hp5e78z2atCTeeCtvbXf6mSree1HStl1UIHCqs1hEhgLvA2HACWCTMWaQiNQHPjHGXCsizYBZjqcEAN8YY15yPL8W8AMQDuwDbjHGpF5qv+6YhloppTxdUZ3Fuh6BUkr5iKICgU46p5RSPk4DgVJK+TgNBEop5eM0ECillI/zyM5iEUkG9rq7HqUQCnh/htNffO14QY/ZV3jqMTc2xlyQkeuRgcBTici6wnrsvZWvHS/oMfsKbztmvTSklFI+TgOBUkr5OA0ErjXl0pt4FV87XtBj9hVedczaR6CUUj5OzwiUUsrHaSBQSikfp4HAhUTkdRHZLiIxIjJLRGq4u05lTURuEZEtImIXEa8ZblcYERksIjtEJF5EymT97fJERKaKyFERiXN3XVxFRBqJyB8iss3Rrh91d52soIHAtRYB7Y0xkcBOYLyb6+MKccAwYJm7K1KWRMQf+IC8VfjaAiNEpK17a1XmPgcGu7sSLpYL/NsY0wboSd6Kix7//6yBwIWMMQuNMbmOu6uAhu6sjysYY7YZY3a4ux4u0B2IN8YkGmOyge+Akizn6nGMMcuAS64f4k2MMYeMMRsct08B24AG7q2V8zQQuM+9wC/uroSyTANgf4H7SXjBF4Qqmog0AToDq91bE+cFuLsC3kZEFgN1C3noGWPMbMc2z5B3ijnNlXUrK8U5Zh8ghZTp2GwvJSJVgRnAv4wxJ91dH2dpILCYMWbgxR4XkZHA9cAA4yVJHJc6Zh+RBDQqcL8hcNBNdVFlSEQqkBcEphljZrq7PlbQS0MuJCKDgbHAjcaYDHfXR1lqLdBCRJqKSCBwGzDHzXVSFhMRAT4Fthlj3nJ3fayigcC1JgLVgEUisklEJru7QmVNRIaKSBLQC5gnIgvcXaey4BgEMBpYQF4H4g/GmC3urVXZEpFvgZVAKxFJEpH73F0nF+gD3AVc6fgMbxKRa91dKWfpFBNKKeXj9IxAKaV8nAYCpZTycRoIlFLKx2kgUEopH6eBQCmlfJwGAqWU8nEaCJRSysf9P2wvkWEG1NOKAAAAAElFTkSuQmCC\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "profile = pcg.Profile([shape0, shape1])\n", - "profile_data = profile.rasterize(0.1)\n", - "plt.plot(profile_data[0], profile_data[1],\"x-\")" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python (test-environment)", - "language": "python", - "name": "test-environment" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.7.5" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} From 18917bd6595ecaa4a708fd4d3749762e0188591e Mon Sep 17 00:00:00 2001 From: vhirtham Date: Mon, 10 Feb 2020 09:33:54 +0100 Subject: [PATCH 157/177] Add tutorial to generate profiles --- tutorials/geometry_01_profiles.ipynb | 960 +++++++++++++++++++++++++++ 1 file changed, 960 insertions(+) create mode 100644 tutorials/geometry_01_profiles.ipynb diff --git a/tutorials/geometry_01_profiles.ipynb b/tutorials/geometry_01_profiles.ipynb new file mode 100644 index 0000000..97a34d9 --- /dev/null +++ b/tutorials/geometry_01_profiles.ipynb @@ -0,0 +1,960 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Introduction\n", + "\n", + "This tutorial is about generating custom 2d profiles using the geometry package. The process can be divided into 3 seperate steps.\n", + "\n", + "- Create segments\n", + "- Create Shapes from segments\n", + "- Create a profile from multiple shapes\n", + "\n", + "Each individual step will be discussed seperately.\n", + "\n", + "Before we can start, we need to import some packages:" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "from mpl_toolkits.mplot3d import Axes3D # noqa: F401 unused import\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "import copy\n", + "\n", + "import mypackage.geometry as geo\n", + "import mypackage.transformations as tr" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Apart from some standard packages and the geometry module, we also import the transformation module. This is explained in detail in another tutorial, but the utilized functionality is rather self explanatory.\n", + "\n", + "# Segments\n", + "\n", + "Segments are small, 2-dimensional objects which define an elementary shape bounded by a starting point and an end point. Arbitrary shapes can be constructed with those basic entities. The simplest one is the line segment.\n", + "\n", + "## Line segments\n", + "\n", + "The `LineSegment` class describes a straight line between 2 points in 2d space. It can be constructed using the class method 'construct_with_points':" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "line_segment_0 = geo.LineSegment.construct_with_points([0, 0], [1, 0])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "It is also possible to create a line segment by calling its constructor directly. It requires a 2x2 matrix as input, where the first column contains the coordinates of the starting point and the second column the coordinates of the end point." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "line_segment_1 = geo.LineSegment([[1, 0], [-1, -1]])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "However, using the constructor directly might cause some confusion when just looking onto the code because of the way a matrix is defined. In the line above one can make the mistake and think the line segment is composed by the two points `[1, 0]` and `[-1, -1]`. But instead its points are `[1, -1]` and `[0, -1]`, because each inner set of brackets define a row of the matrix (This convention is adapted from numpy) and the points are stored in columns. Keep that in mind or use the `construct_with_points` method exclusively.\n", + "\n", + "## Rasterization\n", + "\n", + "Each segment type has a 'rasterize' method which creates a set of data points that lie on the curve defined by the segment. The start and end point are always included. Additionally, all points have the same distance to each other. How large this distance is must be specified using the functions `raster_width` parameter. The data is returned in form of a 2xN matrix.\n", + "\n", + "For a `LineSegment`, all data points are located on the connecting line between start and end point." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(-0.05, 1.05, -1.05, 0.05)" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "# rasterize both segments\n", + "data_line_segment_0 = line_segment_0.rasterize(0.1)\n", + "data_line_segment_1 = line_segment_1.rasterize(0.3)\n", + "\n", + "# plot data\n", + "plt.plot(data_line_segment_0[0], data_line_segment_0[1], \"o\")\n", + "plt.plot(data_line_segment_1[0], data_line_segment_1[1], \"ro\")\n", + "plt.axis(\"equal\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In the above example we rasterized both segments using different `raster_width`. While it is possible to create a set of equidistant data points using `raster_width=0.1` for a line segment of length 1, it can not be done with `raster_width=0.3`. So the function calculates an effective raster width which is as close as possible to the specified one and uses this instead. For our example with `raster_width=0.3` the effective raster width is 1/3.\n", + "\n", + "In case the raster width exceeds the length of the segment, it is automatically clipped to the segment length. In result, the data contains only the start and end point. Negative values or `0` will trigger an exception.\n", + "\n", + "## Arc segments\n", + "\n", + "Another default segment type is the `ArcSegment`. As the name suggests, it represents an arc between the defined start and end point. There are several ways to create an arc segment. The first one is using three points, the start and end point of the segment and the center point of the arc. The corresponding function is the `construct_with_points` method. It takes 4 parameters. The first 3 are the points. The fourth one is a bool which defines the winding order of the arc. If it is set to `True` the arc connects the start point to the end point using an counter clockwise arc. Otherwise both points are connected with a clockwise arc segment. This is shown in the following example, were we create two arc segment with identical points and their center point being the coordinate systems origin:" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(-1.0999720781865128,\n", + " 1.0994136419167673,\n", + " -1.0994136419167673,\n", + " 1.0999720781865128)" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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90VcZmNawwJBAvPDZLKmajWR2LFjB/Pnz2ft2rVs3bqVJ554ot7xn/3sZzz88MN89NFHDBkyhPvvvx+AvLw8fv7zn7N161Y2bNhAr1696s7ZsGEDt99+O6+++iqZmZmMGTOGd999lx07dpCZmcn69esB2LhxIzk5OfXer6CggKeffpqSkhLWr19Phw4dWL16NXv27OFvf/sbJSUlbNmyhXXr1kVUbkPd7OLxEyfa+sUuWSBIYH5YmrKtajVr167lpptuonv37gCce+65dce++eYbKioqGD9+PAAzZsxg3bp1fPfddxw8eJAbbrgBCIyN7+iMQtm1axf5+fm89tprnO8MRxw7dizr1q1j3bp13HHHHWzbto2ysjLOPffcRguJjBkzhjvvvJMnn3ySiooKzjjjDFavXs3q1asZMWIEF198MR9//DF79uyJqNxJL2h2sQStX2zBoGkWCBJY7dKU3dIkYZembKtajaq2eCSLNrFaX69evUhLS+PDDz+s2zdu3DjWr1/P+vXrmTBhAj169GDFihWMHTu20flz585l0aJFHD9+nJycHD7++GNUlXvuuYeSkhJKSkrYu3cvt956a4vybBqw2cWtYoEgwV0/ojf/MaEjf1/wf3h37sSECgLQdrWayy+/nBdffJEjR44A8PXXX9cd69y5M127dq1rynn++ecZP34855xzDn369GHFisBqqdXV1RxzvlS6dOnC66+/zq9//WuKi4sB6Nu3L1999RV79uwhMzOTyy67jD/84Q8hA8Gnn37KkCFDuPvuu8nOzubjjz/mqquuYvHixVRWVgJw8OBBym2d3cjY7OJWsVFDJqZqA5fXo4Yuuugi5s2bx/jx40lJSWHEiBH079+/7vjSpUu5/fbbOXbsGJmZmTz77LNAICjcdttt3HvvvbRv356XXnqp7pyMjAxee+01rr76ahYvXsyll17KpZdeyunTgT6OsWPHcs8993DZZZc1ys/jjz9OUVERKSkpDBo0iKuvvprU1FR27drFqFGjAOjUqRPLli0jPT09orInNZtd3Dqh1q+M94etWVxfvJVh586drTrv22+/9Tgn0eeHMqi2rhyt/dw9FWb9Yl22LNY5a7VEWbMYEZksIrtFZK+IzA1xPFVEXnCOvy8i/YOO3ePs3y0iV3mRn2Sxaf5TfNk1g3G5E/myawab5j8V6ywZE1tBs4s1aP1im1jWtIgDgYikAE8DVwODgJtFZFCDZLcCR1X1n4DHgIedcwcRWOP4ImAy8J/O9UwzNs1/isH3z6FnRTntUHpWlDP4/jkWDIxxZhe/vXatrV/skhc1gpHAXlXdp6ongEJgSoM0U4ClzuuXgcslMKRjClCoqtWq+ndgr3M904y+Bb+nw8nqevs6nKymb8HvY5QjY0yi8qKzuDdwIGi7FLg0XBoNLHb/DdDN2b+xwbkhewlFJB/Ih0CnXe3IjUhVVlZ6dq1oGldxOOT+9IrDMS9P586d62bOtsTp06dbdV488UMZoHXlqKqqivnfHkD6mjVkLlrE+PJyqtLT2TdrFuWTQi6bnhCi8R3lRSAINVi74YDscGncnBvYqboQWAiQnZ2tEyZMaEEWwysuLsara0XTl1160LOi8VDD8i49Yl6eXbt2teqe/H64l78fygCtK0daWhojRoxooxy5tHw5PPZY3VyCtEOHGPTYYwy68MKEbSKKxneUF01DpUDfoO0+QFm4NCJyBtAZ+NrluSaEA3N+y/H2qfX2HW+fyoE5v41RjoyJAzahrFW8CASbgAtEZICInEmg83dlgzQrgRnO65uAtc5QppXANGdU0QDgAuBvHuTJ9y6ZN5vt9xXwZZd0ahC+7JLO9vsKuGTe7FhnLSk8/vjjdZPNvNbw9hRuTZgwgc2bN7f4vFmzZrFz585WvWfcsQllrRJxIFDVU8Bs4A1gF/Ciqu4QkQdE5Don2TNANxHZC9wJzHXO3QG8COwE/hf4uaqebvgeJrRL5s2m59FDrCtaS8+jhxI3CDg3CevUuXPC3CSsNYGgduJZvFm0aBGDBjUc6Jegwk0cswllTfJkHoGqrlLVH6rqD1R1vrPvXlVd6byuUtWpqvpPqjpSVfcFnTvfOW+gqv7Vi/yYBNJGNwkLdQvqzz77jMsvv5yhQ4dy+eWX87nzK3HmzJm8/PLLdefW/iKvbZu96aab+NGPfkReXh6qypNPPklZWRm5ubnk5uYCsHr1akaNGsXYsWOZOnVq3W0j+vfvzwMPPMBll11Wb5YywKFDh7jhhhsYNmwYw4YNY8OGDfWOqyp33XUXgwcPZsiQIbzwwgt1xx555BGGDBnCsGHDmDu3/tSdmpoaZsyYwW9+8xtefPFF7rzzTgCeeOIJMjMzgcAtL2pnQNfWJE6fPs3MmTMZPHgwOTk5PPbYY3VpJ0+eTFZWFmPHjuXjjz9u7cfS9my5ytYJNcss3h82s7i+eCtDi2aY9utXfxZo7aNfv1a///bt2/WHP/yhHj58WFVVjxw5oqqq1157rS5ZskRVVZ955hmdMmWKqqrOmDFDX3rppbrzzzrrLFUN/Luec845euDAAT19+rTm5OTo+vXrnWz3q7v+4cOHdezYsVpZWanffvutLliwQO+///66dA8//HDIfP74xz/Wxx57TFVVT506pRUVFfXe/+WXX9ZJkybpqVOn9Msvv9S+fftqWVmZrlq1SkeNGqX/+Mc/6pVv/Pjx+t577+m0adP0wQcfVFXVL774QrOzs1VV9cYbb9Ts7GwtLS3VJUuW6Ny5c+vO27Rpk27evFknTZqkqoGZxUePHlVV1YkTJ+onn3yiqqobN27U3NzckOWJi5nFqoFZxP36aY1I4O8ogWcVqybQzGITQ8uXkzNtGrRrlzDNKvW0QZtuuFtQv/fee9xyyy1AYPGYd955p9lrjRw5kj59+tCuXTuGDx/O/v37G6XZuHEjO3fuZMyYMYwZM4alS5fyWdD9bn7yk5+Ezecdd9wBQEpKCp07d653/J133uHmm28mJSWFjIwMxo8fz6ZNm1izZg3/8i//UneL7OBbbN92220MHjyYeU7naM+ePamsrOS7777jwIED3HLLLaxbt47169c3ujleZmYm+/bt4xe/+AVvvvkm55xzDpWVlWzYsIGpU6cyfPhwbrvtNr744otm/91iyiaUtZgFgkTmNKukHToU+B2diPdeb4M2XXV5C+raNGeccQY1NTV15544caIuTWrq9yOzUlJSOHXqVMj3u+KKKygpKeHdd99l586dPPPMM3XHzzrrrFaXI9z+cOUbPXo0RUVFVFVV1e0bNWoUzz77LAMHDmTs2LGsX7+e9957jzFjxtQ7t2vXrmzdupUJEybwpz/9iVmzZlFTU0OXLl3qbpVdUlLCrl27WlUeE78sECQyPwyVa4M23XC3oB49ejSFhYUALF++vK6NvH///mzZsgWAV199lZMnTzb7HmeffXbdhKucnBzeffdd9u7dC8CxY8f45JNPXOXzj3/8IxDoSP7222/rHR83bhwvvPACp0+f5vDhw6xbt46RI0dy5ZVXsnjx4rrO6uBbbN96661cc801TJ06tS5ojRs3joKCAsaNG8eIESMo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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "# create arc segments\n", + "arc_segment_0_cw = geo.ArcSegment.construct_with_points([-1, 0], [0, 1], [0, 0], False)\n", + "arc_segment_0_ccw = geo.ArcSegment.construct_with_points([-1, 0], [0, 1], [0, 0], True)\n", + "\n", + "# rasterize segments\n", + "data_arc_segment_0_cw = arc_segment_0_cw.rasterize(0.1)\n", + "data_arc_segment_0_ccw = arc_segment_0_ccw.rasterize(0.1)\n", + "\n", + "# plot data\n", + "plt.plot(data_arc_segment_0_cw[0], data_arc_segment_0_cw[1], 'o', label=\"clockwise\")\n", + "plt.plot(data_arc_segment_0_ccw[0], data_arc_segment_0_ccw[1], 'ro', label=\"counter clockwise\")\n", + "plt.grid()\n", + "plt.legend()\n", + "plt.axis(\"equal\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The provided center point must have the same distance to the start and the end point of the segment. Otherwise we would get an elliptical arch instead of an arc. If this condition is not fulfilled, an exception is raised.\n", + "\n", + "Another method to construct an arc segment is to provide the segments start and end point and a radius. To do so, use the \"construct_with_radius\" method. It takes 5 parameters. The first to parameters are the segments start and end point. The third is the radius. Since there usually exist two possible center points with the same radius, the fourth parameter is a bool defining which center point should be selected. If `True`, the point to the left of the line connecting start and end point is selected. Otherwise the right point is selected. The fifth parameter determines the winding of the arc segment.\n", + "\n", + "Here is a short example:" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(-0.09997207818651273,\n", + " 2.099413641916767,\n", + " -1.0994136419167673,\n", + " 1.0999720781865128)" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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vZdvfetbXH755QHzLVu2jGeffZYTJ07w5ZdfsmnTJrKysgD4j//4j5DvN2nSJACGDx/OoUOHOHDgAKtXr+aVV14BYOTIkezduzfgYuvXXXcdLVq0oG/fvuwKsNB4KPTo0YMHHniA+++/nzfffJNbbrmFnJwcVqxYUStfsJDcPXv2pFevXmRnZwNOeOodO3bw1VdfceDAASd4HTB58mTeeOONugXo0IEhgwY5obGPHWPSNdewets2Wn/66anQ2Nu3kzdmDO/94x9c5+sBc/x4nXtHJTS2VzV9sKigRliE1SIQkRtFZKOInBSROvErfPKNEZHNIrJVRGb5pPcSkb+JyBYRWSoicRuH+Pzzz2ft2rX079+f++67j4ceeqhOns8++4z58+fzzjvvsGHDBr75zW9y9OjRmuP1hYH2J9Sw0P754FS4aiBwKGk/Zs+eTXZ2ds2Ls5qPPvqI2267jf/8z//kxhtv5Oc//3mdc6tDcq9bt45169axdetWbrnlljrlqA6dHSj8djCkTZtTE9N69EBOO622PcHCWrduXfveR49yIlBo7EAeSA0NyIaaz+uavvX/G2EQbtfQJ8ANwHvBMohIS+BJ4GqgLzBJRPq6h38BPKaq5wH7gVvCLE/U2LlzJ6mpqdx0003MnDmTv//970DtMNAHDx6kXbt2dOjQgV27dgWu6brUF6IanH56gNWrV3PGGWfQoUMHhg8fziL3RVJUVERaWhpnnHFGSOWv735z586teZGDU8vPysri/vvvZ8SIEWzatIlf//rXXHTRRXXODTUkdzVnnnkmHTp0YPXq1QA19gSiwdDY55zD4rff5opBg06dJALnnFP7QqGGxj58OLSXdygv+fpq+r6E6uljUUGNMAira0hVSyBwrdOHIcBWVd3m5l0CjBOREmAk8B0334vAj4GnwylTtPj444+5++67adGiBa1bt+bppx0z8vPzufrqq+nSpQuFhYUMHDiQiy66iN69e9esNBaIqVOnMmPGjKCDlh07duTf//3fOXjwYM36Aj/+8Y+5+eabycrKIjU1lRdffDHk8mdlZdGqVSsGDBjA1KlT6x0n6NSpE3/605/IyMho8LpXXnklJSUlNSuptW/fnoULF9Y7QP/888/XDBZfddVVQfNVh8b++OOPGT58ONdffz0tWrSoCY2tqlyTm8u40aOd2r4I9OhRt6UQ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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "# create arc segments\n", + "arc_segment_1_rcp = geo.ArcSegment.construct_with_radius([0, 0], [1, 1], 1, False, True)\n", + "arc_segment_1_lcp = geo.ArcSegment.construct_with_radius([0, 0], [1, 1], 1, True, True)\n", + "\n", + "# rasterize segments\n", + "data_arc_segment_1_rcp = arc_segment_1_rcp.rasterize(0.1)\n", + "data_arc_segment_1_lcp = arc_segment_1_lcp.rasterize(0.1)\n", + "\n", + "# extract center points\n", + "center_point_right = arc_segment_1_rcp.point_center\n", + "center_point_left = arc_segment_1_lcp.point_center\n", + "\n", + "# plot everything\n", + "plt.plot(data_arc_segment_1_rcp[0], data_arc_segment_1_rcp[1], \"ro\")\n", + "plt.plot(data_arc_segment_1_lcp[0], data_arc_segment_1_lcp[1], \"bo\")\n", + "plt.plot(center_point_right[0], center_point_right[1], \"rx\", label=\"right center point\")\n", + "plt.plot(center_point_left[0], center_point_left[1], \"bx\", label=\"left center point\")\n", + "plt.plot([0,1],[0,1], \"g\", label = \"start point -> end point\")\n", + "plt.grid()\n", + "plt.legend(loc=\"lower left\")\n", + "plt.axis(\"equal\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As for the `LineSegment`, an `ArcSegment` can also be constructed using the class constructor. It takes a 2x3 matrix and a winding order as parameters, were the columns are start, end and center point. However, for the same reasons mentioned in the section about the `LineSegment`, it is better to use the \"constuct\" methods.\n", + "\n", + "# Shape\n", + "\n", + "The `Shape` class is a container class that stores multiple segments and ensures that they are connected to each other. The start point of a segment must always be identical to the end point of the previous segment, if it is not the first. If you try to add a segment which does not fulfill this requirement, an exception is raised.\n", + "\n", + "The class constructor takes a single segment or a list of segments as parameter. You can also provide an empty list or no parameter at all, which results in an empty `Shape`. You can also always add more segments using the `add_segments` function. As for the constructor, you can provide single segments or lists of segments:" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "# create some segments\n", + "segment_0 = geo.LineSegment.construct_with_points([0,0],[0,1])\n", + "segment_1 = geo.LineSegment.construct_with_points([0,1],[1,1])\n", + "segment_2 = geo.ArcSegment.construct_with_points([1,1],[3,1],[2,1],False)\n", + "segment_3 = geo.LineSegment.construct_with_points([3,1],[4,1])\n", + "segment_4 = geo.LineSegment.construct_with_points([4,1],[4,0])\n", + "segment_5 = geo.LineSegment.construct_with_points([4,0],[0,0])\n", + "\n", + "\n", + "# create a shape\n", + "shape_0 = geo.Shape([segment_0, segment_1])\n", + "\n", + "# add more segments to the shape\n", + "shape_0.add_segments(segment_2) # single segment\n", + "shape_0.add_segments([segment_3, segment_4, segment_5]) # list of segments" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Like the segments, the `Shape` class has a `rasterize` function, which works quite similar. Internally it just calls the `rasterize` methods of all its segments using the provided `raster_width` and returns the combined data. Because of this behaviour, the effective raster width may vary for each individual segment of the shape. An extreme example is to take a `raster_width` which is bigger than the length of the largest segment. Each segment will clip it to its own length and return just its start and end point:" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(-0.2, 4.2, -0.09993582535855264, 2.098652332529605)" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAXQAAAD4CAYAAAD8Zh1EAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4xLjEsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy8QZhcZAAAgAElEQVR4nO3de3wU9b3/8deXJNxvCopiMIGqKNgIhJtVxIgVBKu1AmLVU461FC+1HusFb7V6qj9Fe45VFKy2tVTKTevlWNAWs0Bbq4AFKVclkEqEKmK5KgGS7++P726yWTZ7STa7M5v38/HYR3ZmvjP7yWT2k9nvfPY7xlqLiIj4X4tMByAiIqmhhC4ikiWU0EVEsoQSuohIllBCFxHJErmZeuGuXbvawsLCBq27f/9+2rVrl9qAUsCrcYF3Y1NcyVFcycnGuN57773PrLXHRF1orc3Io7i42DZUIBBo8LpNyatxWevd2BRXchRXcrIxLmCFrSevqstFRCRLKKGLiGQJJXQRkSyRsYui0Rw6dIiKigoOHDgQs12nTp1Yv359mqJKnFfjAu/G1qlTJ7Zs2UJ+fj55eXmZDkfE1zyV0CsqKujQoQOFhYUYY+ptt3fvXjp06JDGyBLj1bjAu7Ht2bOHgwcPUlFRQc+ePTMdjoivearL5cCBA3Tp0iVmMpfsYoyhS5cucT+ViUh8nkrogJJ5M6S/uUhqeC6hi4hIwyihp9Drr7/OunXrmvQ1tm3bxtixY6MuO/fcc1mxYgUADz30UM388vJyhgwZkvJY3njjDXr37s1JJ53Eww8/HLXN0qVLGTBgALm5ubz44ospj0FEavk2oU+dCoFA3XmBgJufCtZaqqurk1qnIQn98OHDSbXv3r17QokxPKE3haqqKm644QYWLlzIunXrmD17dtTf/cQTT+T555/n29/+dpPGIyI+TuiDBsH48bVJPRBw04MGNXyb5eXlnHbaaVx//fUMGDCArVu3ct111zFw4ED69u3LfffdV9N2ypQp9OnTh6KiIm699VbefvttFixYwG233Ua/fv0oKyujrKyMUaNGUVxczLBhw9iwYQMAEydO5JZbbqGkpIQ77rijTgyjR49m9erVAPTv358HHngAgHvvvZfnnnuO8vJyTj/9dAC+/PJLJkyYQFFREZdffjlffvllTWxffvkl/fr148orrwRcAv7e975H3759ueCCC2raNtSyZcs46aST6NWrFy1btmTChAm8+uqrR7QrLCykqKiIFi18e6iJ+IanyhbD3XwzrFoVfVlVVRtycqB7dxg5Eo4/HrZvh9NOg/vvd49o+vWDxx+P/bobN27k17/+NU8//TQADz74IEcffTRVVVWMGDGC1atXk5+fz8svv8yGDRswxrBr1y46d+7M6NGjufTSS2u6REaMGMGMGTM4+eSTeffdd7n++uspLS0F4IMPPmDRokXk5OTUef1zzjmHP//5zxQWFpKbm8tf//pXAP7yl79w1VVX1Wk7ffp02rZty+rVq1m9ejUDBgwA4OGHH2batGmsCu7A8vJyysrKmDt3Ls8++yzjx4/npZdeOmJ7s2bN4tFHHz1in5x00klHfCr4+OOP6dGjR810fn4+7777buydKyJNyrMJPRFHHeWS+UcfwYknuunGKigoYOjQoTXT8+bN4xe/+AWHDx9m+/btrFu3jj59+tC6dWuuvfZaxowZw0UXXXTEdvbt28fbb7/NuHHjauZVVlbWPB83btwRyRxg2LBhPPHEE/Ts2ZMxY8bwpz/9iS+++ILy8nJ69+5NeXl5TdulS5dy0003AVBUVERRUVHM36tfv34AFBcX19lOyJVXXllzRh+PjXIvWlWriGSWZxN6rDPpvXu/pEOHDjXdLPfeC9Onw333QUlJ4143fEjLLVu28Nhjj7F8+XKOOuooJk6cyIEDB8jNzWXZsmW89dZbzJkzh2nTptWceYdUV1fTuXPnmrPkWK8TbtCgQaxYsYJevXrx9a9/nc8++4xnn32W4uLiqO0TTaKtWrWqeZ6TkxO1yyWZM/T8/Hy2bt1aM11RUUH37t0TikVEmoZvOzZDyXzePHjgAfczvE89Ffbs2UO7du3o1KkTn3zyCQsXLgTc2ffu3bsZPXo0jz/+eE3Sbt++PXv37gWgY8eO9OzZk/nz5wPujPb999+P+5otW7akR48ezJs3j6FDhzJs2DAee+wxhg0bdkTbc845h1mzZgGwZs2amr53gLy8PA4dOpTU73vllVeyatWqIx7RLsIOGjSIDz/8kC1btnDw4EHmzJnDxRdfnNTriUhq+TahL1/uknjojLykxE0vX5661zjjjDPo378/ffv25ZprruGss84C3NfoL7roIoqKihg+fDj/+7//C8DY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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "# rasterize shape with different raster width\n", + "shape_0_data = shape_0.rasterize(0.1)\n", + "shape_0_data_clipped = shape_0.rasterize(10000)\n", + "\n", + "# plot data\n", + "plt.plot(shape_0_data[0], shape_0_data[1], 'bx-', label=\"raster width = 0.1\")\n", + "plt.plot(shape_0_data_clipped[0], shape_0_data_clipped[1], 'ro-', label=\"clipped\")\n", + "plt.grid()\n", + "plt.legend(loc=\"upper left\")\n", + "plt.axis(\"equal\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Note that the shared points of the connected segments occur only once in the data array even though the rasterize methods of two neighboring segments both return their shared point. Additionally, the `Shape` recognizes that the first point of the first segment is identical to the last point of the last segment and includes it only once.\n", + "To be sure let's print the number of points of the clipped data, which must be 6 if no duplications occur:" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "6\n" + ] + } + ], + "source": [ + "print(shape_0_data_clipped.shape[1])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "However, the shape does not generally filter duplications. If two segments have a common point which is not the first/last point of the shape and one or more segments were added between them, then this point will usually be included more than once. If this is a problem, you need to take care of that yourself by using `numpy.unique` or an equivalent function.\n", + "\n", + "# Adding multiple line segments to a shape\n", + "\n", + "So far, creating a `Shape` required us to generate all the segments in advance, which is a little bit tedious. Additionally, because all segments need to be connected to each other, a lot of points occur at least twice during the segments creation. By using the `add_line_segments` function of the `Shape`, it is no longer necessary to create line segments in advance.\n", + "\n", + "The function takes a list of points and creates line segments from them, which are subsequently added to the `Shape`. The segments are created by traversing the list in ascending order and using the corresponding point as the segments end point. The start point is taken from the previous segment in the `Shape`. In case the `Shape` is empty, the first two points of the list are used to create the new segment:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(-0.05, 1.05, -0.07500000000000001, 1.575)" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "# create first 2 segments with an empty shape\n", + "shape_1 = geo.Shape()\n", + "shape_1.add_line_segments([[0, 0], [0, 1], [1, 1]])\n", + "\n", + "# add 6 more segments to the existing shape\n", + "shape_1.add_line_segments([[0.5, 1.5], [0, 1], [1, 0], [0, 0], [1, 1], [1, 0]])\n", + "\n", + "# rasterize data\n", + "data_shape_1 = shape_1.rasterize(0.05)\n", + "\n", + "# plot\n", + "plt.plot(data_shape_1[0], data_shape_1[1], 'rx')\n", + "plt.axis(\"equal\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "There is no equivalent function for arc segments. They can be constructed in different ways and during construction you need to provide more data than just the segments start and end point. Hence an \"add_arc_segments\" function would have a bloated and probably non-intuitive interface.\n", + "\n", + "# Transformations\n", + "\n", + "Consider you want to generate the shape of the previous example but rotated about a certain angle. Calculating the new position of the points and creating a new shape would be rather wearisome. Instead one can use shape's transformation functions. These are `translate`, `transform`, `apply_translation` and `apply_transformation`. The functions with the preceding \"apply_\" perform in-place transformations while the other ones return a transformed copy of the original object.\n", + "\n", + "`translate` and `apply_translation` take a 2d vector as parameter. It defines the translation which is applied to every segment of the shape:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(-2.15, 1.15, -0.225, 4.725)" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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0xQtsRhwww3ApUv2vLraVtINDbafeirvYpfqrn2joxZPZ6d1K05MAO+9B6xcaZ2LROWOnYklbMUK4J577KZHdXXA++8D999v3zt/furvYpfurn3nz9s5991nk83r6oAzZ4BNm+zPQVTOmKhLWEUFsHu31YHPnbO6765dwL59doOkixe9+0O7m/kXq307sY3dDSmor7c4tm0D9u6Nj7e11Z5XVBQ2FqLphrc5LWHt7VY6OHIE+OQngWeesTKIql24c0n5s5+1VWxbmzftpVC3HU28raorebhJMidOeN+rrbWLiS+84MW7caPFxhZyKnW8zWmZam21JN3aajsoWlstCa5YEX8XO/+YrELfxS7dXftqaux1V7O+7jqLzx+vi5+orKW6ypjPg7s+omH9em/fcnOzt495/Xr7vtt9cfKk10noWrv9u0BOnsy+jbuvL/79tbVe67jrXvT//CDxEpUyFGrXB00vyVbU/hWqf67hrl2FmxYeZLr5rl12rn8eY6Z4icoVE3UJGx8HHnjAGkuam+34wAP2OpB8Wnh1tTd5Jddp4ammm7shBammm2eKl6hcMVGXsLNngccfB+6911ao995rz/3dg4nTwg8etAt8uUwLzzTd/Phxb0gBcOV087NnbUeKP959+67sdiQqN0zUJW5sDDh82MoNhw/bc78g08KXLctcBklW7li2LPvp5pcvA4cOWbyHDtlzonLHHaolzt07A7CjSPLzXMJMnBbuOhgbG60pxT+g1s/t7nDljmXLvI5D9z5/iSXVhHN/jP7YicoZV9QlLhYDbr8deOQRO8Zi6c9PnBY+MpK+3by9PX1b+MhIdtPNZ84EOjos3o4OtpATAUzUJc21kO/daxfn9u615+lashOnhQ8NpW83b21N3xbuT+KZppuvWGEt4/542UJOxERd0vwt5G67W5CW7MRp4anazTs77fzOztRt4dlMN881XqJSx0RdwgYHrRPQv93NPQ8isQyyZ4+3z3rZMpsQvmWLHZct89rC9+zJrtxRqHiJShUTdQnLt4EksQwC2D7oWMxq0A8+CPz1X9txdNReT2wLz1TuKGS8RKWKN2UqYcluypTrTY6GhoANG+wi39tv2xEArr4aePNN+7q3F1iwAHjoIfuZqXZ2TEW8RNMNb8pUpgq5Qt250+5l/fDDwKOPWlIGLEmL2PO/+Rvgq1+1e0rv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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "shape_2 = shape_1.translate([-2,3])\n", + "\n", + "# rasterize data\n", + "data_shape_2 = shape_2.rasterize(0.05)\n", + "\n", + "plt.plot(data_shape_1[0], data_shape_1[1], 'rx')\n", + "plt.plot(data_shape_2[0], data_shape_2[1], 'bx')\n", + "plt.axis(\"equal\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`transform` and `apply_transformation` expect a 2x2 transformation matrix as parameter, which is applied to all segments of the shape:" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(-3.114775866404782,\n", + " 1.9628520777580993,\n", + " -0.3497595264191645,\n", + " 7.344950054802455)" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "# create a transformation matrix which distorts and rotates the shape\n", + "angle = np.pi/6\n", + "s = np.sin(angle)\n", + "c = np.cos(angle)\n", + "\n", + "distortion_matrix = np.array([[2, 0],[0,5]])\n", + "rotation_matrix = np.array([[c, -s], [s, c]])\n", + "transformation_matrix = np.matmul(rotation_matrix,distortion_matrix)\n", + "\n", + "# get transformed shape\n", + "shape_3 = shape_1.transform(transformation_matrix)\n", + "\n", + "# rasterize\n", + "data_shape_3 = shape_3.rasterize(0.05)\n", + "\n", + "# plot all shapes\n", + "plt.plot(data_shape_1[0], data_shape_1[1], 'rx')\n", + "plt.plot(data_shape_2[0], data_shape_2[1], 'bx')\n", + "plt.plot(data_shape_3[0], data_shape_3[1], 'gx')\n", + "plt.axis(\"equal\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Reflections\n", + "\n", + "Since a lot of profiles consist of symmetric shapes, there is a `reflect` and a `apply_reflection` function. Similar to the other transformation functions `reflect` creates a reflected copy of the shape while `apply_reflection` modifies the original shape. These function perform a reflection across an arbitrary line. The first parameter of those functions is the normal of the line of reflection. The second parameter is optional and specifies line of reflection's distance to the coordinate systems origin. The default distance is 0. Here are 3 examples:" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(-1.1, 1.1, -1.15, 2.15)" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "# reflect across the y axis\n", + "shape_4 = shape_1.reflect([1,0])\n", + "\n", + "# rasterize \n", + "data_shape_4 = shape_4.rasterize(0.05)\n", + "\n", + "# plot all shapes\n", + "plt.plot(data_shape_1[0], data_shape_1[1], 'rx', label=\"original\")\n", + "plt.plot(data_shape_4[0], data_shape_4[1], 'bx', label=\"reflected\")\n", + "plt.plot([0, 0], [-1, 2], 'y--', label=\"line of reflection\")\n", + "plt.legend()\n", + "plt.axis(\"equal\")" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(-1.15, 2.15, -0.2, 4.2)" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "# reflect across the horizontal line with y = 2\n", + "shape_5 = shape_1.reflect([0, 1], 2)\n", + "\n", + "# rasterize \n", + "data_shape_5 = shape_5.rasterize(0.05)\n", + "\n", + "# plot all shapes\n", + "plt.plot(data_shape_1[0], data_shape_1[1], 'rx', label=\"original\")\n", + "plt.plot(data_shape_5[0], data_shape_5[1], 'bx', label=\"reflected\")\n", + "plt.plot([-1, 2], [2, 2], 'y--', label=\"line of reflection\")\n", + "plt.legend(loc=\"lower left\")\n", + "plt.axis(\"equal\")" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(-1.15, 2.15, -0.15000000000000002, 3.15)" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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0CEKXRCEsGApc6ezDodjQGvBhu3btzN06FCys7PZu3cv559/fkD9P/3003z++ed+3VNVVRXw63UFRKELQpyxcGH7oNDycnM+VDQ3N1Jb+wFOZ127BdDnnnuOmTNnsmfPHtLS0gJ+jUAUendHFLogxBnhyp7bNlVuNs899wIFBcVceGE+kyZNoqamhieeeII1a9Ywb948br755jb3Nzc3c8899zBy5EiGDx/OH//4x9ZrCxcuZNiwYVxwwQXcd999PP/88+zcuZObb76Z7Oxs6urq2LVrF5dffjl5eXlcffXVfPHFF4DJAnnBBRcwZswYHnnkkeAeMtbxFnEU7k0iRdsjcjCIHAzhjBQtK9O6b1+tS0rMX/d6F4HwySefaKWUfv311/WXX36hL7nkIl1TU6O11vq3v/2tvv/++7XWWt9666167dq1rfcMHTpUa631H//4Rz1//nyttdb19fU6Ly9Pv/3223rjxo16zJgxura2VmutdXV1tdZa68svv1zv2LFDa611Q0ODHjNmjP7qq6+01lqvXr1aT5kyRWut9bBhw3RFRYXWWuuZM2e2vl5XQCJFBUHolDBkz8XpbOLMMwcwatSF/OUvG9m7dx8XX3wxAA0NDYwZM6bD+19++WXefvttnn/+ecAk3zpw4ADbt29nypQp9OjRA4CTTjqp3b379u3j3XffpaCgADCz/VNPPZWjR49y5MgRLr/8cgCKiorYtGlT8A8bo4hCF4Q4JNTZc53OJurrP6FHj2Sczjq01hQUFLBq1Sqf+9Ba87vf/Y6rr7669dyxY8fYtm0bndWc11ozdOhQXn/99Tbnjxw50um93QmxoQtCnBHq7Lkub5Z6EhIcJCamM3r0aP7v//6P/fv3A3D8+HE+/PDDDvu5+uqrWb58eWuhig8//JDa2lquuuoqVqxYwfHjxwH4+mtT3bJnz54cO3YMgHPPPZdDhw61KvTGxkbee+89evfuTWZmJq+++ipgFmS7M6LQBSHOCGX2XLtrogkaMirllFNO4emnn+amm25i+PDhjB49urUWqDd++tOfct5555Gbm8v555/PHXfcQVNTE+PHj29JszuC7Ozs1hJ0t912G9OmTSM7O5vm5maef/55fvnLX3LBBReQnZ3Na6+9BsBTTz3FnXfeyZgxY4LyqukSeDOuh3uTRdH2iBwMIgdDV0if29RUq48d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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "# reflect across a line with slope 1 containing the points (0,1) and (0.5, 1.5)\n", + "shape_6 = shape_1.reflect([-1, 1], 1/np.sqrt(2))\n", + "\n", + "# rasterize \n", + "data_shape_6 = shape_6.rasterize(0.05)\n", + "\n", + "# plot all shapes\n", + "plt.plot(data_shape_1[0], data_shape_1[1], 'rx', label=\"original\")\n", + "plt.plot(data_shape_6[0], data_shape_6[1], 'bx', label=\"reflected\")\n", + "plt.plot([-1, 2], [0, 3], 'y--', label=\"line of reflection\")\n", + "plt.grid()\n", + "plt.legend(loc=\"upper right\")\n", + "plt.axis(\"equal\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In the last example we wanted to reflect across a line that is not parallel to one of the coordinate systems axes. Providing the normal and distance to the origin gets a little bit more complicated for this case, since they aren't obvious anymore and need to be calculated. As an alternative to the previous ones, there also exist the methods `reflect_across_line` and `apply_reflection_across_line`. They perform a reflection across a line which is defined by its start and end point. Here is the alternative version of the previous example:" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(-1.15, 2.15, -0.15000000000000002, 3.15)" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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hS4IQliwlLnTeQKHo09jwYYtW7YwYMAAhgwZwq5duxg4cGBA/T/33HN8/fXXft1TWloa8Ou1B0ShC0KMsWBB86DQoiJzPlTU19dSUfEpTueJZgugL7zwAjNmzGDnzp106tQp4NcIRKF3dEShC0KMEa7suU1T5Q7hhRdeJidnKhddlM3EiRMpLy/nmWeeYdWqVcydO5fbbrutyf319fU8+OCDDBs2jEGDBvGnP/2p8dqCBQtIT09n8ODBPPzww7z00kts27aN2267jSFDhnDixAm2b9/OyJEjycrKYsyYMXzzzTeAyQI5ePBgRowYwZNPPhncQ0Y73iKOwr1JpGhzRA4GkYMhnJGihYVa9+ihdX6++ete7yIQ9u/fr5VS+p///Kf+9ttv9GWXXaLLy8u11lr/7ne/04888ojWWus777xTr169uvGeAQMGaK21/tOf/qTnzZuntda6qqpKZ2Vl6Q8++ECvXbtWjxgxQldUVGittS4rK9Naaz1y5Ei9detWrbXWNTU1esSIEfq7777TWmu9cuVKfdddd2mttU5PT9fFxcVaa61nzJjR+HrtAYkUFQShVcKQPRens44zz+zDxRdfxD/+sZZdu3Zz6aWXAlBTU8OIESNavH/Dhg188MEHvPTSS4BJvrVv3z62bNnCXXfdRefOnQE4+eSTm927e/duPvroI3JycgAz2z/11FM5duwYR48eZeTIkQDk5eWxbt264B82ShGFLggxSKiz5zqddVRV7adz50SczhNorcnJyWHFihU+96G15ve//z1jxoxpPHf8+HE2b95MazXntdYMGDCAf/7zn03OHz16tNV7OxJiQxeEGCPU2XNd3ixVxMU5iI/vwvDhw3n77bfZu3cvAJWVlXz22Wct9jNmzBiWLl3aWKjis88+o6Kigquvvpply5ZRWVkJwOHDprpl165dOX78OADnn38+Bw8ebFTotbW1fPzxx3Tv3p3U1FTeeustwCzIdmREoQtCjBHK7Ll210QTNGRUSs+ePXnuuee49dZbGTRoEMOHD2+sBeqNe+65hwsvvJDMzEwGDhzIvffeS11dHWPHjm1IszuUIUOGNJagmzRpElOmTGHIkCHU19fz0ksv8etf/5rBgwczZMgQ3nnnHQCeffZZ7rvvPkaMGBGUV027wJtxPdybLIo2R+RgEDkY2kP63Lq6Cn38+E5dW3s0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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "# reflect across a line with slope 1 containing the points (0,1) and (0.5, 1.5)\n", + "point_0 = [0, 1]\n", + "point_1 = [0.5, 1.5]\n", + "shape_7 = shape_1.reflect_across_line(point_0, point_1)\n", + "\n", + "# rasterize \n", + "data_shape_7 = shape_7.rasterize(0.05)\n", + "\n", + "# plot all shapes\n", + "plt.plot(data_shape_1[0], data_shape_1[1], 'rx', label=\"original\")\n", + "plt.plot(data_shape_7[0], data_shape_7[1], 'bx', label=\"reflected\")\n", + "plt.plot([point_0[0], point_1[0]], [point_0[1], point_1[1]], 'co', label=\"points\")\n", + "plt.plot([-1, 2], [0, 3], 'y--', label=\"line of reflection\")\n", + "plt.grid()\n", + "plt.legend(loc=\"upper right\")\n", + "plt.axis(\"equal\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Here is another example:" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(-3.508243243243243, 2.2622972972972972, -4.8, 1.8)" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "# reflect across a line with slope 1 containing the points (-2,1) and (2, -4.5)\n", + "point_0 = [-2, 1]\n", + "point_1 = [2, -4.5]\n", + "shape_8 = shape_1.reflect_across_line(point_0, point_1)\n", + "\n", + "# rasterize \n", + "data_shape_8 = shape_8.rasterize(0.05)\n", + "\n", + "# plot all shapes\n", + "plt.plot(data_shape_1[0], data_shape_1[1], 'rx', label=\"original\")\n", + "plt.plot(data_shape_8[0], data_shape_8[1], 'bx', label=\"reflected\")\n", + "plt.plot([point_0[0], point_1[0]], [point_0[1], point_1[1]], 'co', label=\"points\")\n", + "plt.plot([point_0[0], point_1[0]], [point_0[1], point_1[1]], 'y--', label=\"line of reflection\")\n", + "plt.grid()\n", + "plt.legend(loc=\"lower left\")\n", + "plt.axis(\"equal\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Profiles\n", + "\n", + "A `Profile` is a container class that stores multiple shapes. It represents the cross section of an assembly. One can add shapes to a `Profile` via its constructor or the `add_shapes` method. Both accept single shapes and lists of shapes. Like segments and the `Shape` class, a `Profile` also has a `rasterize` with identical functionality. Lets create a symmetric profile and rasterize it:" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(-0.3500000000000001, 7.350000000000001, -1.1, 1.1)" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "# create shapes\n", + "shape_9 = geo.Shape()\n", + "shape_9.add_line_segments([[0, 1], [1.5, 1], [3, 0.25], [3, -1], [0, -1], [0, 1]])\n", + "shape_10 = shape_9.reflect([1,0], 3.5)\n", + "\n", + "# create profile\n", + "profile_0 = geo.Profile(shape_9)\n", + "profile_0.add_shapes(shape_10)\n", + "\n", + "# rasterize\n", + "data_profile_0 = profile_0.rasterize(0.1)\n", + "\n", + "# plot\n", + "plt.plot(data_profile_0[0], data_profile_0[1], 'rx')\n", + "plt.grid()\n", + "plt.axis(\"equal\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "That's already everything you need to know about profiles\n", + "\n", + "# Custom segments\n", + "\n", + "It might happen that lines and arcs are not enough to sufficiently describe a certain shape or profile. For this reason it is possible to define custom segment types. A segment which is useable with the `Shape` and `Profile` classes is a python class that needs at least a `rasterize` method and a `point_start` and `point_end` property. However, you want to use the Shapes transformation functions, you also need to define the segments `translate`, `transform`, `apply_translation` and `apply_transformation` functions. \n", + "\n", + "As a small example, we will create a sinusoidal wave segment. It should generate a sinusoidal wave in normal direction to the line from the segments start to its end. Since the start and end points must be included in the segments shape (otherwise we get visual gaps during rasterization) we can only use waves with wave lengths $N\\pi$, were $N$ is the number half waves. Additionally we will add the option to vary the waves amplitude. The constructor of the class looks as follows:\n", + "\n", + "~~~ python\n", + " def __init__(self, point_start, point_end, num_half_waves, amplitude=1):\n", + " self._points = np.array([point_start, point_end], float).transpose()\n", + " self._num_half_waves = num_half_waves\n", + "\n", + " vector_start_end = self.point_end - self.point_start\n", + " normal = np.array([-vector_start_end[1], vector_start_end[0]], float)\n", + "\n", + " self._amplitude_vector = np.ndarray((2, 1), float, tf.normalize(normal)) * amplitude\n", + "~~~\n", + "\n", + "The points are stored as columns in a 2x2 matrix. Instead of storing the amplitude value itself, the normal to the vector `point_start`->`point_end` is calculated. Its length is adjusted so that it is equal to the amplitude. We call this vector `_amplitude_vector` and store it.\n", + "The reason for this is, that certain transformations (uneven scaling) distort the wave. As we will see later, this distortion can be memoized using the `_amplitude_vector`.\n", + "\n", + "The implementation of `point_start` and `point_end` properties should be self explanatory:\n", + "\n", + "~~~ python\n", + " @property\n", + " def point_start(self):\n", + " return self._points[:, 0]\n", + "\n", + " @property\n", + " def point_end(self):\n", + " return self._points[:, 1]\n", + "~~~\n", + "\n", + "The last mandatory method a segment must provide is the `rasterize`\n" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [], + "source": [ + "class SinWaveSegmentBase:\n", + " def __init__(self, point_start, point_end, num_half_waves, amplitude=1):\n", + " self._points = np.array([point_start, point_end], float).transpose()\n", + " self._num_half_waves = num_half_waves\n", + "\n", + " vector_start_end = self.point_end - self.point_start\n", + " normal = np.array([-vector_start_end[1], vector_start_end[0]], float)\n", + "\n", + " self._amplitude_vector = np.ndarray((2, 1), float, tf.normalize(normal)) * amplitude\n", + " \n", + " @property\n", + " def point_start(self):\n", + " return self._points[:, 0]\n", + "\n", + " @property\n", + " def point_end(self):\n", + " return self._points[:, 1]\n", + "\n", + " def _calculate_points_on_line(self, raster_width):\n", + " # calculate distance between start and end point\n", + " vector_start_end = self.point_end - self.point_start\n", + " distance = np.linalg.norm(vector_start_end)\n", + "\n", + " # normalized effective raster width\n", + " num_raster_segments = np.round(distance / raster_width)\n", + " nerw = 1. / num_raster_segments\n", + " \n", + " # linear interpolation of the points\n", + " weights = np.arange(0, 1 + 0.5 * nerw, nerw)\n", + " weight_matrix = np.array([1 - weights, weights])\n", + " return np.matmul(self._points, weight_matrix)\n", + "\n", + " def _calculate_offsets(self, num_raster_segments):\n", + " total_range = np.pi * self._num_half_waves\n", + " increment = total_range / num_raster_segments\n", + " \n", + " angles = np.arange(0, total_range + 0.5 * increment, increment)\n", + " return np.sin(angles) * self._amplitude_vector\n", + "\n", + " def rasterize(self, raster_width):\n", + " points_on_line = self._calculate_points_on_line(raster_width)\n", + " offsets = self._calculate_offsets(points_on_line.shape[1] - 1)\n", + "\n", + " return points_on_line + offsets\n", + "\n", + " " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python (test-environment)", + "language": "python", + "name": "test-environment" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.5" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} From fdf7ed376ae342a4599cd86e86ba960792c69a01 Mon Sep 17 00:00:00 2001 From: vhirtham Date: Mon, 10 Feb 2020 13:58:32 +0100 Subject: [PATCH 158/177] Finish first tutorial --- tutorials/geometry_01_profiles.ipynb | 254 ++++++++++++++++++++++++++- 1 file changed, 248 insertions(+), 6 deletions(-) diff --git a/tutorials/geometry_01_profiles.ipynb b/tutorials/geometry_01_profiles.ipynb index 97a34d9..fcebe69 100644 --- a/tutorials/geometry_01_profiles.ipynb +++ b/tutorials/geometry_01_profiles.ipynb @@ -29,7 +29,7 @@ "import copy\n", "\n", "import mypackage.geometry as geo\n", - "import mypackage.transformations as tr" + "import mypackage.transformations as tf" ] }, { @@ -536,7 +536,7 @@ "source": [ "# Reflections\n", "\n", - "Since a lot of profiles consist of symmetric shapes, there is a `reflect` and a `apply_reflection` function. Similar to the other transformation functions `reflect` creates a reflected copy of the shape while `apply_reflection` modifies the original shape. These function perform a reflection across an arbitrary line. The first parameter of those functions is the normal of the line of reflection. The second parameter is optional and specifies line of reflection's distance to the coordinate systems origin. The default distance is 0. Here are 3 examples:" + "Since a lot of profiles consist of symmetric shapes, there is a `reflect` and a `apply_reflection` function. Similar to the other transformation functions `reflect` creates a reflected copy of the shape while `apply_reflection` modifies the original shape. These function perform a reflection across an arbitrary line. The first parameter of those functions is the normal of the line of reflection. The second parameter is optional and specifies the line of reflection's distance to the coordinate systems origin. The default distance is 0. Here are 3 examples:" ] }, { @@ -871,7 +871,54 @@ " return self._points[:, 1]\n", "~~~\n", "\n", - "The last mandatory method a segment must provide is the `rasterize`\n" + "The last mandatory method a segment must provide is the `rasterize` method with a single parameter, the `raster_width`:\n", + "\n", + "~~~ python\n", + " def rasterize(self, raster_width):\n", + " points_on_line = self._calculate_points_on_line(raster_width)\n", + " offsets = self._calculate_offsets(points_on_line.shape[1] - 1)\n", + "\n", + " return points_on_line + offsets\n", + "~~~ \n", + "\n", + "The implementation is split into 2 parts. First an equidistant set of points on the line `point_start`->`point_end` is generated. Afterwards the offset in direction of the `_amplitude_vector` are calculated and added. \n", + "\n", + "Note, that the point are not equidistant anymore, after the offset is applied. This is not consistent with the implementation of the `LineSegment` and the `ArcSegment`, but we want to keep things simple here.\n", + "\n", + "The implementation of the function `_calculate_points_on_line` is:\n", + "\n", + "~~~ python\n", + " def _calculate_points_on_line(self, raster_width):\n", + " # calculate distance between start and end point\n", + " vector_start_end = self.point_end - self.point_start\n", + " distance = np.linalg.norm(vector_start_end)\n", + "\n", + " # normalized effective raster width\n", + " num_raster_segments = np.round(distance / raster_width)\n", + " nerw = 1. / num_raster_segments\n", + " \n", + " # linear interpolation of the points\n", + " weights = np.arange(0, 1 + 0.5 * nerw, nerw)\n", + " weight_matrix = np.array([1 - weights, weights])\n", + " return np.matmul(self._points, weight_matrix)\n", + "~~~\n", + "\n", + "First, the length of the vector `point_start`->`point_end` is calculated. Using the specified `raster_width`, one can calculate how many points will fit into the raster. Afterwards, linear interpolation between start and end point is used.\n", + "\n", + "The offsets are determined as follows:\n", + "\n", + "~~~ python\n", + " def _calculate_offsets(self, num_raster_segments):\n", + " total_range = np.pi * self._num_half_waves\n", + " increment = total_range / num_raster_segments\n", + " \n", + " angles = np.arange(0, total_range + 0.5 * increment, increment)\n", + " return np.sin(angles) * self._amplitude_vector\n", + "~~~\n", + "\n", + "It calculates the sine for each point multiplies it with the amplitude vector. Note that the points positions are not needed here, since we know that they areequidistant.\n", + "\n", + "Here is the fully implemented class:" ] }, { @@ -880,7 +927,7 @@ "metadata": {}, "outputs": [], "source": [ - "class SinWaveSegmentBase:\n", + "class SineWaveSegmentBase:\n", " def __init__(self, point_start, point_end, num_half_waves, amplitude=1):\n", " self._points = np.array([point_start, point_end], float).transpose()\n", " self._num_half_waves = num_half_waves\n", @@ -928,12 +975,207 @@ " " ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now we generate a shape, add an instance of this segment and rasterize it." + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(-0.7939352559383557,\n", + " 10.513996916949445,\n", + " -5.513996916949446,\n", + " 5.793935255938355)" + ] + }, + "execution_count": 26, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "# create custom segment\n", + "sw_base_segment = SineWaveSegmentBase([0, 0], [5, 5], 5, 1)\n", + "\n", + "# create a shape\n", + "shape_11 = geo.Shape(sw_base_segment)\n", + "shape_11.add_line_segments([[10, 0], [5, -5], [0, 0]])\n", + "\n", + "# rasterize\n", + "data_shape_11 = shape_11.rasterize(0.25)\n", + "\n", + "\n", + "# plot data\n", + "plt.plot(data_shape_11[0], data_shape_11[1], 'rx')\n", + "plt.grid()\n", + "plt.axis(\"equal\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As you can see, we have successfully implemented our custom segment. \n", + "\n", + "If we want to apply transformations to the shape, we need to add the corresponding functionality to the segment type. We generate a new class which inherets from the `SineWaveSegmentBase`. Then we add the following functions, which perform \"in-place\" transformations:\n", + "\n", + "~~~ python\n", + "def apply_translation(self, vector):\n", + " self._points += np.ndarray((2, 1), float, np.array(vector, float))\n", + " return self\n", + "\n", + "def apply_transformation(self, matrix):\n", + " self._points = np.matmul(matrix, self._points)\n", + " self._amplitude_vector = np.matmul(matrix, self._amplitude_vector)\n", + " return self\n", + "~~~\n", + "\n", + "The translation is applied by adding the passed vector to the segments start and end point. A transformation is done by multiplying the `_points` matrix with the passed transformation matrix. Additionally, the `_amplitude_vector` must also be multiplied by the matrix.\n", + "\n", + "We can use those 2 functions to add the other two functions that return a transformed copy:\n", + "\n", + "~~~ python\n", + " def translate(self, vector):\n", + " new_segment = copy.deepcopy(self)\n", + " return new_segment.apply_translation(vector)\n", + "\n", + " def transform(self, matrix):\n", + " new_segment = copy.deepcopy(self)\n", + " return new_segment.apply_transformation(matrix)\n", + "~~~\n", + "\n", + "Here is the full class:" + ] + }, { "cell_type": "code", - "execution_count": null, + "execution_count": 31, "metadata": {}, "outputs": [], - "source": [] + "source": [ + "class SineWaveSegmentFull(SineWaveSegmentBase):\n", + " def __init__(self, point_start, point_end, num_half_waves, amplitude=1):\n", + " super().__init__(point_start, point_end, num_half_waves, amplitude)\n", + "\n", + " def apply_translation(self, vector):\n", + " self._points += np.ndarray((2, 1), float, np.array(vector, float))\n", + " return self\n", + "\n", + " def translate(self, vector):\n", + " new_segment = copy.deepcopy(self)\n", + " return new_segment.apply_translation(vector)\n", + "\n", + " def apply_transformation(self, matrix):\n", + " self._points = np.matmul(matrix, self._points)\n", + " self._amplitude_vector = np.matmul(matrix, self._amplitude_vector)\n", + " return self\n", + "\n", + " def transform(self, matrix):\n", + " new_segment = copy.deepcopy(self)\n", + " return new_segment.apply_transformation(matrix)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Here are some examples that show that our implementation works:" + ] + }, + { + "cell_type": "code", + "execution_count": 56, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 56, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "# create custom segment\n", + "sw_full_segment = SineWaveSegmentFull([0, 0], [5, 5], 4, 1)\n", + "\n", + "# create a shape\n", + "shape_12 = geo.Shape(sw_full_segment)\n", + "shape_12.add_line_segments([[10, 0], [5, -5], [0, 0]])\n", + "\n", + "# create translated copy\n", + "shape_13 = shape_12.translate([-14,7])\n", + "\n", + "# create a distorted and translated copy\n", + "shape_14 = shape_12.transform([[2, 0], [0, 0.5]])\n", + "shape_14.apply_translation([0,10]) \n", + "\n", + "# create a rotated and translated copy\n", + "s = np.sin(-np.pi / 4)\n", + "c = np.cos(-np.pi / 4)\n", + "shape_15 = shape_12.transform([[c, -s], [s, c]])\n", + "shape_15.apply_translation([25,10]) \n", + "\n", + "# create a reflected copy\n", + "point_0 = [-5,0]\n", + "point_1 = [0,-10]\n", + "shape_16 = shape_12.reflect_across_line(point_0, point_1)\n", + "\n", + "\n", + "# rasterize\n", + "data_shape_12 = shape_12.rasterize(0.25)\n", + "data_shape_13 = shape_13.rasterize(0.25)\n", + "data_shape_14 = shape_14.rasterize(0.25)\n", + "data_shape_15 = shape_15.rasterize(0.25)\n", + "data_shape_16 = shape_16.rasterize(0.25)\n", + "\n", + "# plot data\n", + "plt.plot(data_shape_12[0], data_shape_12[1], 'rx', label=\"original\")\n", + "plt.plot(data_shape_13[0], data_shape_13[1], 'bx', label=\"translated\")\n", + "plt.plot(data_shape_14[0], data_shape_14[1], 'gx', label=\"distorted and translated\")\n", + "plt.plot(data_shape_15[0], data_shape_15[1], 'kx', label=\"rotated and translated\")\n", + "plt.plot(data_shape_16[0], data_shape_16[1], 'mx', label=\"reflected\")\n", + "plt.plot([point_0[0], point_1[0]], [point_0[1], point_1[1]], 'co', label=\"points\")\n", + "plt.plot([point_0[0], point_1[0]], [point_0[1], point_1[1]], 'y--', label=\"line of reflection\")\n", + "plt.grid()\n", + "plt.axis(\"equal\")\n", + "plt.legend(loc=\"lower right\")" + ] } ], "metadata": { From 4724c3ae757c83292210d382149fab1e54322904 Mon Sep 17 00:00:00 2001 From: vhirtham Date: Mon, 10 Feb 2020 14:16:49 +0100 Subject: [PATCH 159/177] Update tutorial about profiles --- tutorials/geometry_01_profiles.ipynb | 54 ++++++++++++++-------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/tutorials/geometry_01_profiles.ipynb b/tutorials/geometry_01_profiles.ipynb index fcebe69..c9226c2 100644 --- a/tutorials/geometry_01_profiles.ipynb +++ b/tutorials/geometry_01_profiles.ipynb @@ -170,8 +170,8 @@ ], "source": [ "# create arc segments\n", - "arc_segment_0_cw = geo.ArcSegment.construct_with_points([-1, 0], [0, 1], [0, 0], False)\n", - "arc_segment_0_ccw = geo.ArcSegment.construct_with_points([-1, 0], [0, 1], [0, 0], True)\n", + "arc_segment_0_cw = geo.ArcSegment.construct_with_points([-1, 0], [0, 1], point_center=[0, 0], arc_winding_ccw=False)\n", + "arc_segment_0_ccw = geo.ArcSegment.construct_with_points([-1, 0], [0, 1], point_center=[0, 0], arc_winding_ccw=True)\n", "\n", "# rasterize segments\n", "data_arc_segment_0_cw = arc_segment_0_cw.rasterize(0.1)\n", @@ -229,8 +229,8 @@ ], "source": [ "# create arc segments\n", - "arc_segment_1_rcp = geo.ArcSegment.construct_with_radius([0, 0], [1, 1], 1, False, True)\n", - "arc_segment_1_lcp = geo.ArcSegment.construct_with_radius([0, 0], [1, 1], 1, True, True)\n", + "arc_segment_1_rcp = geo.ArcSegment.construct_with_radius([0, 0], [1, 1], radius=1, center_left_of_line=False, arc_winding_ccw=True)\n", + "arc_segment_1_lcp = geo.ArcSegment.construct_with_radius([0, 0], [1, 1], radius=1, center_left_of_line=True, arc_winding_ccw=True)\n", "\n", "# rasterize segments\n", "data_arc_segment_1_rcp = arc_segment_1_rcp.rasterize(0.1)\n", @@ -271,12 +271,12 @@ "outputs": [], "source": [ "# create some segments\n", - "segment_0 = geo.LineSegment.construct_with_points([0,0],[0,1])\n", - "segment_1 = geo.LineSegment.construct_with_points([0,1],[1,1])\n", - "segment_2 = geo.ArcSegment.construct_with_points([1,1],[3,1],[2,1],False)\n", - "segment_3 = geo.LineSegment.construct_with_points([3,1],[4,1])\n", - "segment_4 = geo.LineSegment.construct_with_points([4,1],[4,0])\n", - "segment_5 = geo.LineSegment.construct_with_points([4,0],[0,0])\n", + "segment_0 = geo.LineSegment.construct_with_points([0,0], [0,1])\n", + "segment_1 = geo.LineSegment.construct_with_points([0,1], [1,1])\n", + "segment_2 = geo.ArcSegment.construct_with_points([1,1], [3,1], point_center=[2,1], arc_winding_ccw=False)\n", + "segment_3 = geo.LineSegment.construct_with_points([3,1], [4,1])\n", + "segment_4 = geo.LineSegment.construct_with_points([4,1], [4,0])\n", + "segment_5 = geo.LineSegment.construct_with_points([4,0], [0,0])\n", "\n", "\n", "# create a shape\n", @@ -569,7 +569,7 @@ ], "source": [ "# reflect across the y axis\n", - "shape_4 = shape_1.reflect([1,0])\n", + "shape_4 = shape_1.reflect(reflection_normal=[1, 0])\n", "\n", "# rasterize \n", "data_shape_4 = shape_4.rasterize(0.05)\n", @@ -612,7 +612,7 @@ ], "source": [ "# reflect across the horizontal line with y = 2\n", - "shape_5 = shape_1.reflect([0, 1], 2)\n", + "shape_5 = shape_1.reflect(reflection_normal=[0, 1], distance_to_origin=2)\n", "\n", "# rasterize \n", "data_shape_5 = shape_5.rasterize(0.05)\n", @@ -655,7 +655,7 @@ ], "source": [ "# reflect across a line with slope 1 containing the points (0,1) and (0.5, 1.5)\n", - "shape_6 = shape_1.reflect([-1, 1], 1/np.sqrt(2))\n", + "shape_6 = shape_1.reflect(reflection_normal=[-1, 1], distance_to_origin=1/np.sqrt(2))\n", "\n", "# rasterize \n", "data_shape_6 = shape_6.rasterize(0.05)\n", @@ -818,7 +818,7 @@ "# create shapes\n", "shape_9 = geo.Shape()\n", "shape_9.add_line_segments([[0, 1], [1.5, 1], [3, 0.25], [3, -1], [0, -1], [0, 1]])\n", - "shape_10 = shape_9.reflect([1,0], 3.5)\n", + "shape_10 = shape_9.reflect(reflection_normal=[1,0], distance_to_origin=3.5)\n", "\n", "# create profile\n", "profile_0 = geo.Profile(shape_9)\n", @@ -984,7 +984,7 @@ }, { "cell_type": "code", - "execution_count": 26, + "execution_count": 20, "metadata": {}, "outputs": [ { @@ -996,7 +996,7 @@ " 5.793935255938355)" ] }, - "execution_count": 26, + "execution_count": 20, "metadata": {}, "output_type": "execute_result" }, @@ -1069,7 +1069,7 @@ }, { "cell_type": "code", - "execution_count": 31, + "execution_count": 21, "metadata": {}, "outputs": [], "source": [ @@ -1104,22 +1104,22 @@ }, { "cell_type": "code", - "execution_count": 56, + "execution_count": 22, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "" + "" ] }, - "execution_count": 56, + "execution_count": 22, "metadata": {}, "output_type": "execute_result" }, { "data": { - "image/png": "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\n", + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAXkAAAD4CAYAAAAJmJb0AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4xLjEsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy8QZhcZAAAgAElEQVR4nOydeVxU1fvH35cdFEHB3BXNXUPcNU2hzHK3bLGstPKrlb+yxcwWy0zLrTIzU1vU0rLSNLc2DTTLPVFxwQVRUXMFZBn25/fHlWlAlhmYYYbhvH3d18i9555z7mX4zJnnPudzNBFBoVAoFM6Ji707oFAoFArboUReoVAonBgl8gqFQuHEKJFXKBQKJ0aJvEKhUDgxbvbugCmBgYESFBRk726UmJSUFCpVqmTvbtiVin4PKvr1g7oHUPb3YM+ePZdFpHpBxxxK5IOCgti9e7e9u1FiIiIiCA0NtXc37EpFvwcV/fpB3QMo+3ugadqpwo6pcI1CoVA4MUrkFQqFwolRIq9QKBROjBJ5hUKhcGKUyCsUCoUTo0ReoVAonBgl8gqFQuHEKJFXKBQKJ0aJvEKhUDgxVhF5TdO+1DTtoqZpUSb7JmmadlbTtMjrW19rtKVQKBQK87HWSH4xcHcB+z8UkZDr2wYrtaVQKBQKM7GKyIvIFuCqNepSKBQKhfXQrLXGq6ZpQcA6EWl9/edJwAjgGrAbeElE4gs4bxQwCqBGjRrtly9fbpX+2IPk5GQqV65s727YlYp+Dyr69YO6B1D29yAsLGyPiHQo6JgtRb4GcBkQ4B2glog8UVQdHTp0EOVCWb6p6Pegol8/qHsAdnGhLFTkbZZdIyIXRCRbRHKAz4BOtmpLoVAoFAVjM5HXNK2WyY/3AFGFlVUoFAqFbbDKoiGapn0LhAKBmqbFAW8BoZqmhaCHa2KB0dZoS6FQKBTmYxWRF5GHCtj9hTXqVigUCkXJUTNeFQqFwolRIq9QKBROjBJ5hUKhcGKUyCsUCoUTo0ReoVAonBgl8gqFQuHEKJFXKBQKJ0aJvEKhUDgxSuQVCoXCibHKjFeFQgE/HfmJCf9MoFdqL0KDQunRoAfVK1W3d7cUFRwl8gqHJisni82xm/nz9J9kZmeSIzm4aC5U865GoE+gcatZuSa1fWvj7uput7428G+AIdvA3F1zmbtrLgCtb2pNaINQJfoKu6FEXuHQPL3uaT7f+zkAbi5uuGguZOdkky3ZN5R10Vyo7VubelXqUd+vPvX96hPkH0Sjqo24uerNNPBvgIerh836GlIzhC87fEly7WSm/zWdv8/8TdTFKKIuRinRV9gNJfI25vRp+OADmDQJ/P3t3ZvyR90qdQF4psMzvH/X+3i5eSEiJGUkcTn1MpdTL3Mp5RLnk89zJvEMp6+d5nTiafac38PqI6tJz0431uWiuVCvSj0aVW1kFH7j/6vdTFWvqmiaVqr+umguDGw2kIHNBvLX6b+Y/td01h5dazzmqrmyKHKREv0y5MqVK2zevBl3d3d69+6Np6en1eo+c+YMu3btonLlytxxxx24urparW5roUTexhw+DB99pG9JSVDBV0WzmPHdxvNv8r/M2z2P32J+45Vur/Bo8KNU8axCFc8qNKraqNBzcySHf5P/JSY+hhNXTxATH0NMgv7/dUfXcSHlQp7yfp5+3FztuvD7N/rv/1UbUd+vPm4ulv25dKvfjTX113Dw4kFm/j2TZQeWERMfw4TuE+hevzvb47YTERuhRN+GpKenc9ttt3H48GEAHnjgAb777jur1H3p0iXatWvH5cuXAXj11Vd59913rVK3NbHa8n/WwBmX/xMBl+s5TG3bwpYtzi30tlr27Nfjv/LaH6/xz/l/qFW5Fi92fZFR7UdRxbNKietMzkjmZPxJ/UMg/kSe15PxJ8nMyTSWddVcaeDfIO/o3+T/fl5+QNHXf+zKMcb9Po410WsIqRnC74/+TqBPIJnZmew5v4eI2AgiYiPYenorKZkpQPkUfUda/u+NN95g6tSpfPXVVxw6dIhp06axatUqBg8eXOq6hw4dyo8//si6dev46quvWL58OTt37qRdu3YOtfyfEnkTzp2DAQP0144doVMnGDIEWrQw7/zCfrG7dul1AfToAevXO6/Q2/LNLSJsOrmJaVunsenkJvy9/HmmwzM81/k5alSuYdW2snOyOZt0tsBvATHxMVwxXMlTPsA7gCD/IDwzPLkl6BZq+9amtm9talauiZ+nH5U9KlPJoxJuLm4s3LOQ6X9Np2+Tvqx/eP0NbZd30XcUkT906BDBwcE8+uijLFq0iMzMTDp16sS///7L8ePHqVSpUonr/vnnn+nbty/vvPMOb7zxBvHx8bRs2ZLatWuze/duNm/erES+IOwt8m3bwvHjMGgQ7NkDR47o+wcNggkToEuXos8v6s390EOwfLn+/x49YMMGKMV7zGEpqz/wXWd3Mf2v6fx4+Ec83Tx5PORxxt06rsjwjTVJTEvUhd9k9B+bEMuxf49xTa5xOfVysXXMunMWL936UrHlypvoO4rIv/DCC8ybN4+zZ88SGBgI6H0LCwtj6dKlDBs2rMR1DxgwgH/++YfY2Fjc3fWMrs8//5z//e9/bN++HYPB4DAij4g4zNa+fXuxFzk5IoGBIo899t++ixdF3npLpFo1ERDp0UPk11/1sgURHh5eaP0nToi4u4tUrizi4qLXlZRk1UtwCIq6B7Yg+nK0jPxppLhPdheXt13koRUPSeT5yDLtgym515+WmSax8bGy/cx2+f3E77L68GpZum+pLNq7SH459otEX44ucRsZWRmy7cw2ee/P9+Sur++SSlMrCZMQJiGt57WW/1v/f7Li4Aq5mHzRSldlGWX9HiiIzMxMqVGjhtx777159mdnZ0uDBg3krrvuKnHdFy9eFDc3N3n55Zfz7E9MTBQvLy8ZM2ZMmd8DYLcUoqt2F3bTzZ4iLyLSt69I69Y37k9KEvnwQ5G6dfU7dscdInv23FiuuF/s2LEirq4i7733n9AnJ1un77YiOT1Z1kavlU93fSpTt0yVD7d9KAt3L5Rv9n8j64+ul73n98qF5AuSnZMtIvb7A49LjJNxv46Tyu9WFiYhdy+9WyJORkhOYZ/INsIe1+9oou8IIr9hwwYBZNWqVTcce/3118XFxUXOnTtXorrnzJkjgOzfv/+GYw8++KBUq1ZNfvvttxLVXVKKEnkVrjFh0iSYPBkuXYKAgBuPZ2TA/Pl6mStX4JFHYMoUaNBAP17c19S4OGjUCEaPhm7dYNgw6N7dcWP0OZJD1y+6svPszmLLurm4UbNyTSpLZdrUb2N8INmoaiMa+jeknl89i7NTSkK8IZ5Pd3/K7O2zuZR6ic51OjOh+wQGNhuIi2Z7Fw9HCFXYO7zjCPfg4Ycf5tdff+X8+fN4eOSdGxEdHU3z5s15//33efHFFy2uu1OnTmRkZBAZGXnDsfXr19O/f39jrL6sUDF5MzlwAIKD4Z13oKjfT0ICTJsGs2frPz/3HLz6KuzbV/yb+8kn4dtv4dQp2LTJsYXekGkg6KMgLqZcZOUDK+nbpC9pWWkkZySTnJFMvCGe88nnOZd0zrhFnYoiQUvgVOIpsnKyjHXlZqfkpicaPwCqNqRR1UZWyVHP3/fFkYuZ+fdMTiacpE2NNkwOm8yApgOs2k5+HEHg8lPWom/ve5CWlkZAQACPPvoo8+fPL7BMx44dcXFxYceOHRbVffLkSRo1asTMmTMZN27cDcczMzOpXbs2wcHBbNq0qUT9LwkqJm8BffvqsfmUlOLLnjqlx/A1TY/bP/PMMUlLK/qcw4f18hMn6j9/+61jh24OXjwodd6vI0xCHv3xUYlLjCuyfO5X9czsTDkZf1I2xWySz/d8Lq9tfE2GrhgqnT7rJIEzAo3hhNzN7z0/abegndz//f0y4fcJ8tmez+SPmD/kVMIpYyioJGRmZ8rX+76WxnMaC5OQjgs7yq6zu0pcX3E4QqiiOGwd3rH3Pfjll18EkA0bNhRaZvLkyaJpmly4cMGiuufOnSuAHD16tNAyjz32mPj6+kpWVpZFdZcGVEzefLZs0e/K3Lnmn7N3r8idd+rnNWwosnx54Q9nRUQGD9Y/SHLLmAq9Iz6MvZZ2TSb8PkE83vEQz3c8ZfTa0XLsyrECy5r7B56YliiR5yNl1eFVMuuvWfLMumfkrq/vksZzGovbZLc8HwAe73hIs4+bSd9lfeXZDc/K7G2zZW30Wjl08ZCkZqSa1V5mdqZ88c8XUuf9OuL6tqu8HfG2TeL19ha4kmBt0bf3PXj22WfF29tbUlMLf2/s2bNHAFmyZIlFdffp00eaNGlSZJnvvvtOAPnrr78sqrs0FCXyVgnXaJr2JdAfuCgira/vqwZ8BwQBscADIhJfVD32DteAPnmpWzc9V/7YMXC3wO9q5sx9LF3ahv379Tz7N96A/v3/mwyVy2efwahRev2NG+v7li937NANQEx8DNO2TmPJviVkZmcypOUQXun2Ch1q//ct0Rpf1bNysjiTeIYT8Sc4cfWE/mry/+SM5DzlA30Cqe9X3+hZU69KPWpUrkGgTyDVfaoT6BOIn5cfIsJVw1UGLR/E4cuH2fjoRu5odEep+pofe4cqrEFpwzv2vAciQuPGjWnRogXr1q0rtFxOTg516tShZ8+eLM/NbS6G1NRUAgICGD16NLNzY7UFkJCQQEBAABMmTGDq1KkWX0NJKCpcY60nYYuBucBXJvsmAJtEZJqmaROu//yKldqzGZoGr72mT4r6/HN4+mnzz+3YMZ4XX4SlS/WHuIMGQcuWMH68nief+/wnd2LUrl3/ifzQofrrsGHQr59jCn2jqo1YOGAhb4e+zZwdc5i3ex4rDq3g9oa380q3V7iz0Z1WacfNxY2GVRvSsGpDejXqleeYiHAp9ZJR8E8n6l41Z67pHwrhseFcS79mVjtpWWlW6a+z4e7qTpe6XehStwsTuk+4QfQd2Ybh6NGjxMTEFBgvN8XFxYU+ffqwatUqsrKycHMrXgrDw8NJS0ujb9++RZbz9/endevWbNiwocxEvkgKG+JbuqGP2KNMfo4Gal3/fy0gurg6HCFcI6KHUW67TeSmm0SuXTP/PNOvqZmZIsuWiQQH62GcunVFxowRWbJEZOFCfd/7799Yh6OHbkxJTEuUGVtnSK1ZtYRJSMj8EPm/r/9Pwk+GiyHTYNd+Hb9yXLad2SZro9fKor2LZPa22TJn+xyZu2OuLIlcIv8m/WuTtu0dqigLigrvtPqklcxcOdNufXv//fcFkNjY2GLLrlixQgDZvHmzWXU/88wz4uPjI2nFPXgTkVGjRgkgcXFFP8PKZc6cObJ+/XqzyhYERYRrbJnTVkNEzl//IDmvadpNBRXSNG0UMAqgRo0aRERE2LBL5vPQQ74880x7xoyJ5YknYs06Jzk5OU//a9fWM3B27qzGypV1+fLLKnzyiX7LW7ZMpGnT/URE5LXMrVkTXn+9OlOntqRbt0SmTTuAt/eNtrqOQkc6srjtYjZe2Mj3cd/zU8JPzF0yF3fNnVZVWhHiH0Ib/za0rNISDxfb2fwWRuXr//KQCod3H+Ywh63eXv73gDMiIpwxnOFiwkUyr2XigQcp6OGcC4kXSKySaLd7cODAAW6++WZOnjzJyZMniyzr5eWFq6sr8+fPJycnp8iyIsLKlSsJCQlh27ZtxfYjODgYgNmzZ9OvX78iyxoMBp577jlA/7ZgdQpTf0s3bhzJJ+Q7Hl9cHY4yks/lwQdFfHxEzp41r3xxo7isLJGDB0VWry5+lL58efkZ0eeSk5Mja39fK2uOrJEXf3lR2i1oJ9okTZiEeL7jKaGLQ2VS+CSJOBlh15G+LXHGkXxOTo4cuXRE5u+aL0NXDJWas2oaR+61368tD698WBbuXihHLx+VnJwcu96D5557Tvz9/c0uHxYWJrfcckux5Q4ePCiALFiwwKx6//jjD6lfv74MHjy42LJr1qwRQKpXr25W3QWBnUbyFzRNqyX6KL4WcNGGbdmEd9+FH3+EN9/U4/OlxdVVj9G3bFl82Qcf1F8ffthxY/T50TSNym6VCW0WyoBmAwBISEvgz1N/6vHcUxG8vfltJm2ehKerJ13rdTXGczvX7YyXm5edr0AB+sDv6JWjxt9ZRGwE/yb/C0Bt39rc3vB24++tcbXGNp13YGv69u3Lyy+/zOnTp6lfv36h5dav143k+vTpY1a9mqbRt29fli5dSnp6epEe9rl1d+/e3YKem48tRX4NMByYdv31Jxu2ZRMaNYIxY2DOHHj+eWjdumzbf/BBPdvHkR/GFoe/lz8Dmg1Qou/AVCRRz0+/fv14+eWX+fnnnxk9enSh5TZs2EBwcDD16tUzu+6+ffsyf/58/vzzT3r16lVgGRFhw4YNADfMzLUWVhF5TdO+BUKBQE3T4oC30MX9e03TngROA/dbo62y5o03YNEieOUVXWTLmvKQdWMJSvTtT0UW9fw0b96coKAg1q9fX6jIJyYmsnXr1mIzdvJz++234+npyfr16wsV+aioKM6cOWNxvy3BKiIvIg8Vcsi6ScjFcO4c/O9/ehpkp076dvvt/6UuloSAAD2l8pVX4I8/9PrKGmcTelOU6NseJeqFo2ka/fr1Y9GiRaSkpBToMf/LL7+QlZVV7APU/FSqVInQ0FDWrl3LBx98UOB9XbtWXxrS19e3ZBdgBk6z/F9GBoSF6UJfv77u1y4CderAiy/q4l/S+/jcc/DJJ/Dyy3pue/7JTWXB0KH6h1d5itGXBCX6pUdEOHb1mDGvPSI2gvPJ5wGoVblWhRb1gnjggQf45JNPWL16dYEe80uXLqVOnTp07dq1RHU/+eST7Nixgy75FqQQEZYuXUr37t25dOlSiftfLIU9kbXHVprsmrg4Pff83Xf1n69dE1m7ViQsTN9ftaruF3PpUsnq/+orvZ5lywovUxZZBaZZN47odWPrexBviHfo7B17ZJbk5ORI9OVoWbB7gTy04iHjvAUmIbVm1boh+8XWlKfsGpGiPeZzvePHjx9vUZ259yDXY/6ZZ565oczu3buNGTvNmjWTBx980KI2TKEieNfk5OiTl4YPv/HY9u0i99yjG4P5+opMnWqeAZkp2dkiISEijRvrqZAFUVZvbkdOryzrP3BHE/2yuH5HE/X8lDeRFxF57bXXxMXFRc6fP59n/8cff1yod3xRmN6DBx98UAICAiQ9PT1PmbFjx4qHh4dcvXpViby59O8v0rRp4eZgBw+KDBqkX3WdOiJfflm4YBfEihX6ud9/X/DxsnxzO+rMWHvnidtb9G1x/Y4u6vkpjyJ/+PBhAWTWrFnGfTk5OdKxY0dp06aNxfWZ3oN169bdsIBJenq63HTTTcaVq5TIm8m8efoVFTdLecsWkc6d9bK33CLy889Fu0bmkpUl0qSJSLt2BZcv6ze3Iwq9vUU+P2Ut+ta4/vIm6vkpjyIvItKjRw8JCAgw2g8vW7ZMAJk3b57FdZneg4yMDGnQoIE0a9ZMDAb9ffb2228LIL/++quIKJE3m5QUkerVRfr0Kb5sTo4+Ir/5ZjEu6ffPP8Wf99lnevmCVveyx5vb0YTe0UQ+P7YW/ZJcf3kX9fyUV5E/ePCgeHh4yH333SeHDh2SgIAA6dy5c4l84fPfg1yP+3HjxsnWrVvF3d1dhg4dajyuRN4CpkzRr2rfPvPKp6eLfPSRSECAHrN/9FF9MZDCSEsTqVVLpF+/G4/Z683tSELv6CKfH2uLvjnX72yinp/yKvIiIlOmTBFAAPHw8JCDBw+WqJ6C7sGIESOMdQcGBsrFi//589tS5J0mhTKXZ57Rl+abNg2++ab48h4eeorkY4/B9Om6odj338Ozz8JLL+mGYaZ4esLAgbr/e06OfdIp8+PMefS2pixSNkWKTmkMaximUhodhAkTJtC8eXOuXr1K27ZtaWmOB4mZzJs3jzvuuAODwUDPnj2pXr1sbJmdTuSrVoWnnoIPPoDXX4dWrcw7z98f3ntP/5B44w39/I8/huHD9fz4XN930CdZLVigL/rRrJltrsNS8gv9hg1QwLwORTFYQ/SVqJdfXF1dGTJkiE3q9vb25pFHHrFJ3UXhdCIPMGGCvvrSK69AEYvDFEi9erBkCUycCLNmweLFujnZvfdC3776ik9Z19endiSRh7xC37evEnprYKnokwLRe6KVqCscBqcUeVMrgvBwfSaspTRuDPPn6ys8ffSRPnJfseK/4126gCOu8qaE3rYUJ/px1+Lo1bSXEnWFw+CUIg96TH3uXD3UsnNnyWPnNWvqYZypU+HoUd3W4N9/9TVaHTXunV/oVYzeduQXfWdY41XhXDityHt768L82GP6Q9KHHy5dfS4u0Ly5vpUHVIxeoVAAOEBuiO0YNgxCQvT4+oED9u5N2TN0qO5Jv2UL/PabvXujUCjsgVOLvIsLLF2qx9d79IDdu+3do7Jlxgz49ltd7AcMsHdvFAqFPXBqkQc9hXLhQj1F8s47K47Qz5ihP3geOhS+/hrcnDYwp1AoisLpRR6gQQOIiKg4Qq8EXqFQ5FIhRB4qjtArgVcoFKZUGJGH/4S+alXnFHol8AqFIj8VSuRBF/rwcF3oe/VyHqFXAq9QKAqiwok8/Cf01ao5h9DPnKkEXqFQFEyFFHlwHqGfMQPGj1cCr1AoCqbCijyU/9CNCtEoFIrisLnIa5oWq2naAU3TIjVNczgZNX0YW56EXgm8QqEwh7IayYeJSIiIdCij9iwiV+jLS+hGCbxCoTCXCh2uMaW8xOiVwBfBvn1w9qy9e6FQOBRlIREC/KZpmgALRGSh6UFN00YBowBq1KhBREREGXSpcN5915MXXgghNNSd99/fR7NmSWafm5ycbNP+f/ttPRYuvJnbb7/AyJFH2LpVbNZWSbH1PSiK0OsLB+xcvJjUBg3s0gd7Xr+jYM97EBcXR1ZWlt1/B5beg9TUVC5evGibfhe2+Ku1NqD29debgH1Aj8LKWmMhb2sQGysSFCTi5yeya5f559lyAePp0/UFyocOFcnMtFkzpcauC3mPH6/fJBA5fNguXShvC5nbgvK8kLe1sPQe2HIhb5uHa0Tk3PXXi8AqoJOt2ywtjjYz1ilDNDt2wH336au6/PCDdcIs774L7u76/2+9FY4cKX2dCkU5x6Yir2laJU3TfHP/D/QGomzZprVwFK8bpxT4gwf1m7ppE8yZAw88oC+ue999+tJbJcXV9b9FfePj9fUZldArKji2HsnXALZqmrYP2AmsF5FfbNym1bC30DulwIP+hDspSb+5SUm6sL/6KmzcCJ06wR13wO+/64EXS+ndW98AEhKU0CsqPDYVeRGJEZE217dWIjLVlu3ZAlOhL8usG6cVeIA2bfTXM2fAwwM6dNDXajx9WvdoOHxYF+oePfSwjqXMmAGa9t9KKUroFRUYlUJpBmU9YcqpBR6gXTt92a6dO/Pur1IFxo2Dkydh3jw4dgy6dNHXMDxxwvz627SBwYP1cNDatfo+JfSKCooSeTMpK6F3eoEHfUXxbt30C8zKuvG4pyc8/bQu8m++qcfZW7SA55+HK1fMa+OVV/S4/J9/6uEhUEKvqJAokbcAW2fdVAiBz2XcOIiNhe+/L7yMry+8/bYu9sOHw8cfw803w/TpYDAUXX/nzrqof/CBfo4SekUFRYm8hdjqYWyFEniA/v2hZUtdsIt7wFq7Nnz2GezfD7fdBhMmQLNm8NVXkJNT+HkTJuipmd9/r38TUEKvqIAokS8B1n4YW+EEHvSY/Cuv6ML9i5kJV61a6TH2P/6Am27SR/ft28P69QV/UPTuDQEB+i8LlNArKiRK5EuItWL0FVLgc3noIahfH956y7J0ybAw/aHtN9/oaZL9+0NIiP6zaYxf06Bjx7wPeE2FPixMCb3C6VEiXwryu1dGR/tadH6FFnjQZ6dOnqznyRcVmy8IFxf9Q+LoUViyRBf3YcOgSROYNUtPvUxLAz8/PSUzO/u/c3OFXkQJvcLpUSJfSkzdK196qY3ZI/oKL/C5PPIIBAfrk6HS0y0/390dHnsMDhyAn36CWrV0q4QuXaByZfjuO3jiCX02rClK6J0SV1dXEhIScHFxset2++23W1Q+Ojoa1/zvUStRUaXFquQKfdeumdx5pxu//67P7ykMJfAmuLrqE6Duugs+/VRPkywJLi4wcKC+xcXp3w527tRj8i++WPA5uUIfFqbH6CMioHnzkl6JwgF4+umn8fX1zTVHtBuxsbEEBQVZdM6QIUNs05nCnMvssTmKC2VJ+fbbvyUoSMTfv3D3yvLiJllSSuxA2Lu3SLVqIvHxVu2PWRw6JFKjhr6V0r1SuVCqeyBS9vcAe7pQViRq1kwvMr1SjeCLYMYMffLSe++Vfdsq60bhxCiRtzKF5dErgS+GNm302PpHH8GpU2XfvhJ6hZOiRN4G5M+jf/ZZJxT4vXth4kRYvhxiYkrmGJmfd97R0x4nTix9XSVBCb3CCVEibyNM0yvnznUygd+9G3r2hClT9DTGm2+Ghg1hzhxcirMbKIp69fQHr0uX6h8i9kAJvcLJUCJvQxo0gC1bYMECJxJ4gM8/19MdT56Ef/6B+fP1SU1jx9J16FA9991cI7H8TJigfzK+/LJ1vh2UBCX0CidCibyNqVsXRo1yIoEHPc0wI0N3i2zbFkaP1j/Ntm4lsXVrfQZrgwb6a3KyZXX7+enOk5s26Zu9UEKvcBKUyCssp2NH/TX/Un3duhE1dSpERUG/fvqIvnFjfaRfkKVwYYwerU9qmjbNen0uCUroFU6AEnmF5bRr999s0oJo1Uo/tn07NG2qe8Pfcos+I9WcEIynpz6BadMm+6+iroReUc5RIq+wHG9vfbT93Xd6XL4wOneGzZt1cQd9taaePc1b0m/UKD10M326dfpcGpTQK8oxSuQVJeOFF3QrgfffL7qcpulWAwcO6LYF0dHmLelXpQqMGQMrV8Lx49bte0lQQq8opyiRV5SMOnXg0Ufhiy/g4sXiy7u5wVNP6YKdf0m/ws4fOVIP7/z+u3X7XlJatNC97EEJvaLcoImdjXxM6dChg+y2dwy2FERERBAaGqXnt3sAACAASURBVGrvbpQdR47oqzuNG6dP6cWCe3DunJ598+WX4OEBTz4JL72k59vnIgI1augPcRctss01lIRDh+D22wHIDA8nzsuLtLQ0ANLS0vDy8rJn7+yOuge2uwdeXl7UrVsXd3f3PPs1TdsjIgXaIjpTYp+irGneXB/Nz5mjh1YaNDD/3Nwl/XI/IBYu1LNwHnxQH/F36KDH/hs31nPxHYmWLY3ulXFbtuA7YABBQUFomkZSUhK+vpatK+BsqHtgm3sgIly5coW4uDgamg6GikGFaxSl45139NeSWhE0a6aHfE6e1EM3a9ZAjx76It7NmsG2bXDHHdbrr7W4HqNPCwoi4OJFtOsjeYXCVmiaRkBAgPFbo7nYXOQ1Tbtb07RoTdOOa5o2wdbtVUSWXbhA0LZtuEREELRtG8suXCi7xuvXt44VQZ06+opOZ87A6tX6zNegIH0xkVmzrNZdq9KiBdSqhaZp+gpVpbF0UCjMQNM0i8+xqchrmuYKfAL0AVoCD2ma1tKWbVY0ll24wKjoaE6lpyPAqfR0RkVHl63Qv/qq9awI/P1h0CDdF+fXX+Hdd/UsHkfF3V2fCwBw9CguJVndSqGwIbb+6+kEHBeRGBHJAJYDg2zcZoXi9ZgYUnNy8MLAEFbgSRqpOTm8HhNTdp3w89PDNZs2EbhlS9m16yh4e+try2Zm4nX+vL17UyB9+/YlISGhyDJvvvkmGzduLFH9ERER9O/fv0TnKmyLrR+81gHOmPwcB3Q2LaBp2ihgFECNGjWIiIiwcZdsR3Jycpn3//T118Yc5//4BA1hBfdzOj29TPuitWxJ07vvpsW773I0MZFzAweWWdv2xM/Pj6Rr1/A6fx53IMPXl6ykJHt3y0ju6kDfXZ+dnFRE315++eViyxRGamoqWVlZJCUlkZ2dXaI6nAlb3oO0tDSL/rZtLfIFBZDyfJ8XkYXAQtBTKMtzCqI9Uijrb9vGqfR0oriFPbTjIb5lLQOo6elHaNeuZdoXevTgclgYTT/8kKZNm+pZMk7O4UOH8L10CZKSYMEC3I8cwduaCzKHhMDs2UUW+eCDD/jyyy8BGDlyJIMHD6ZPnz6EhYWxbds2Vq9eTc+ePdm9ezeBgYG88847LFu2jHr16hEYGEj79u0ZN24cI0aMoH///tx3330EBQUxfPhw1q5dS2ZmJj/88APNmzdn586dPP/88xgMBry9vVm0aBHNmjXDx8cHNzc3fH19VXYNts0w8vLyom3btmaXt3W4Jg6oZ/JzXeCcjdusUExt1Aif6zHrJQynGvEM0dYxtVGjsu+MpycHJ02C/v11v5r588u+D2VJVhZcvqwvW1i3LlSqVOZd2LNnD4sWLWLHjh1s376dzz77jPj4eKKjo3nsscfYu3cvDUxSW3fv3s3KlSvZu3cvP/74I0XNSwkMDOSff/7h6aefZtb1h9/Nmzdny5Yt7N27l8mTJ/Paa6/Z/BoVpcPWI/ldQBNN0xoCZ4GhwMM2brNCMaxGDUCPzUelB3NQ68ATLt8TGjjDLv0RDw9YsQLuu08XenDOEX1WFjzyCIwYoT94rVkTZs/GUMaj2K1bt3LPPfdQ6foHzL333suff/5JgwYN6NKlS4HlBw0ahLe3NwADBgwotO57770XgPbt2/Pjjz8CkJiYyPDhwzl27BiappGZmWntS1JYGZuO5EUkC/g/4FfgMPC9iBy0ZZsVkWE1ahDbtSs5oaEMa/Mh1Sq3IDPzkv065OmpC72zjuhzBf6776BqVV3g7URhM9YrFfKtwpIZ7p6engC4urqSdd0qeuLEiYSFhREVFcXatWstztlWlD02z00TkQ0i0lREbhaRqbZur6Lj79+dtm034+VlwexTW+CsQp+VBcOG6QI/c6ZupGZHevTowerVq0lNTSUlJYVVq1Zx2223FVq+e/fuRnFOTk5m/fr1FrWXmJhInTp1AFi8eHFpuq4oIxw4AVlRGtLTzxMfH2HfTjib0OcK/Pff6wI/bpy9e0S7du0YMWIEnTp1onPnzowcOZKqVasWWr5jx44MHDiQNm3acO+999KhQwf8/PzMbm/8+PG8+uqrdOvWjezsbGtcgsLW5KZYOcLWvn17Kc+Eh4fbuwtG9u3rI1u31pCsrJQybbfAe5CWJtK/vwiIfPppmfbHamRmijz4oH4NM2cadx86dChPsWvXrpV1zywmKSlJRERSUlKkffv2smfPHqvWXx7uga2x5T3I/54TEQF2SyG6qkbyTkr9+q+SmXmBc+cW2Lsr5X9Enz9E4wAj+NIwatQoQkJCaNeuHUOGDKFdu3b27pLChigXSifF3/82/P3v4PTp6dSuPRpXVx/7dihX6Mtb1k3uQ9bvv9fdMsu5wAN888039u6CogxRI3knJijoLccZzUP5G9GbZtHMmKF78ygU5Qwl8k6Mv/9tVK3am/T008UXLivKi9CbhmiUwCvKMSpc4+Tccst6XFwc7NecK/RDhjhm6MY0i0YJvKKco0byNiLzaiYHBh/g77p/s6fLHjIuZ9ilH7kCn5y8j+zsVLv0oUA8PfVFuvv1c6wRvWkMfuZMJfCKco8SeRsR91EcV9ZcwT/Un5R9KUQNjCLbYJ+84pSUw+zeHeI4sflccoXeUUI35TCLJiEhgXnz5tm0DXNshCMjI9mwYYPFdYeGhhbpn6MoPUrkbUBWchZnPz5L4KBAWi5tSYtvWnBt+zWOjDhil/5UqtTCmGnjUKN5cJwYff4RfDkQeChc5Mt6olJJRV5hexwsWFv2XN14lYvLL5KdmE3Q5CAqtSi9k+CFpRfIis+i3iu6AWf1e6rT8J2GnHzjJPFPxVM1rPAZibYiKOgtIiN7cO7cAurVe6HM2y8Se6dXWmkm6/PPw5493pSl0/CECRM4ceIEISEhuLu7U7lyZWrVqkVkZCSHDh1i8ODBnDlzhrS0NMaOHcuoUaMAqFy5MmPHjmXdunV4e3vz008/UaNGDX744QfefvttXF1d8fPzY0u+RWAKshpu2LAhb775JgaDga1bt/L8889z//338+yzz3LgwAGysrKYNGkSgwYNwmAw8Pjjj3Po0CFatGiBQS2ZaHMqtMhnXskkalAULh4uSI5wbec12m1rh2dtz1LVm/hnIh51PPDr8t908bov1eXc/HPETIih3fZ2JVqrsTQ4XN58fuwl9OU8D37atGlERUURGRlJREQE/fr1IyoqioYNGwLw5ZdfUq1aNQwGAx07dmTIkCEEBASQkpJCly5dmDp1KuPHj+ezzz7jjTfeYPLkyfz666/UqVOnwJWkcq2G3dzc2LhxI6+99horV65k8uTJ7N69m7lz55KUlMTUqVO5/fbb+fLLL0lISKBTp0706tWLBQsW4OPjw/79+9m/f7+aiFUGVGiRPzv3LDmpObTf2Z6c9Bz29tjLgX4HaLu1La6VSj4cS9qVRJVOeY2rXL1cCXo7iOgno7m86jLV761eyt5bTlDQW+zf34ekpD34+xduYmU38mfdaBqMHm279qycBz97NiQlGey6YEanTp2MAg8wZ84cVq1aBcCZM2c4duwYAQEBeHh4GOPs7du35/fffwegW7dujBgxggceeMBoNWyKuVbDv/32G2vWrDH60KelpXH69Gm2bNnCc889B0BwcDDBwcHWu3hFgVTYmHx2WjZxH8cRMCCASq0q4dvOl1bftyI5Mpkzs84UX0EhZMZnYjhmwLfjjX/oNR6rgU9zH2JeiyEnK6c03S8R/v630bVrnGMKfC6mD2Ofesp2Mfr8D1mdJIvG1GI4IiKCjRs3sm3bNvbt20fbtm2N1sDu7u7Gb5OmVsLz589nypQpnDlzhpCQEK5cuZKnfnOthkWElStXEhkZSWRkJKdPn6ZFixYAZf4ttqJTYUU+eW8yWVeyqPn4f17gAX0DCBwSyJlZZ8i4VLKUx6Td+rqO+UfyAC5uLjSc0hBDtIHLP14uWcdLibu7PyJCWtopu7RvFrZ+GOuAbpIlJXe5vYJITEykatWq+Pj4cOTIEbZv315sfSdOnKBz585MnjyZwMBAzpzJO+ApzGo4fz/uuusuPv74Y6N//d69ewHdGnnZsmUAREVFsX//fvMvVlEiKqzIJ+28Lsad84pxwykNyU7N5tTUkolg8t5kACq3q1zg8cDBgXg38ebMrDMWLeBgTY4fH8uePZ0dL9PGFFsJfTnNoimMgIAAunXrRuvWrY0Lcedy9913k5WVRXBwMBMnTixwpaj8vPzyy9xyyy20bt2aHj160KZNmzzHC7MaDgsL49ChQ4SEhLBy5UomTpxIZmYmwcHBtG7dmokTJwLw9NNPk5ycTHBwMDNmzKBTp05WuAuKIinMntIeW1laDR8cdlD+qvNXgceOjDwiER4Rknoy1aI6w8PD5cjII7L1pq1Flov7NE7CCZf4zfEW1W8t4uO3SHg4cvr0B1av2+p2y9a0Kc7MFHnggRvsgktDebQatjXqHiirYYcgaWcSVToWvKpPg7cagAan37Xc88Vw3IB3Y+8iy9R8rCbuge6liv2XBtNMG4cezYP1RvRONoJXKMylQop8VmJWoQ9HAbzqelFzRE3+/epfMi5YFptPPZZarMi7+rhS+5naXFl7hZQjKRbVby0czqGyKPIL/QIL+2yaRaMEXlHBqJAin3pMH736tCg8V7zeC/WQDOHsJ2fNrzgNMs5m4N2kaJEHqDOmDpqnRtzsOPPrtyK5o/kLF5bZ7dmARZgKvSVZN+XQqkChsCYVUuQNx/VZdkWJsU8zHwIGBnB23lmyU82cIn5OfyluJA/gcZMH/j39ca1sxemRFtK8+WLatt1aflLacoXeXFOz/G6SSuAVFZCKKfLHrot8o6LFuN64emRdyeLfJf+aV/H1Qb85Ig/gWsmVq+uvmle3DfDyqourqxc5ORnk5KTbrR8WYa6pWf40SSfJg1coLKViivxxA551PXH1KXoU7dfND9/Ovpx5/wySbUZIw0KR927ijeGEwby6bURGxmV27GjC2bO2dTK0KsU9jHWiPHiForTYTOQ1TZukadpZTdMir299bdWWpZiTAQP6zLx6L9Uj7UQal9eYMXkpDtwD3XH3dzerH96NvZFMIe1MwbMGywIPj0C8vZuUj0wbUwoT+gqeRTNp0iSjlcCbb77Jxo0bCy27evVqDh06ZHEbJT2vcuWC546UloLqLQsLZigfNsy2Hsl/KCIh1zeH8SE1HDNP5AEC7wnEq6GXeemO54qO8+cnt2xu+MhelKtMG1PyC/0nn6iHrCZMnjyZXr16FXq8JGKdlZVVYpEvS4oS+Ypmw+wUBmXZhmyOP3ecBm82wKueV5FlsxKzyLyUabYYu7i5UPeFuhx/7jiJfyfid6tf4YXPgvdd5ou8TxM9u8dwzAB3mn2a1XF4h8qiMHWv/L//0/fZ6SHr8788z56ze3C1otdwSM0QZt9dhNcwMHXqVL766ivq1atH9erVad++PQAjRoygf//+3HfffUyYMIE1a9bg5uZG7969uffee1mzZg2bN29mypQprFy5kqSkJJ566ilSU1O5+eab+fLLL6latSqhoaHceuut/PXXX/Tu3fuG8wDGjBnDpUuX8PHxYfbs2bRv356TJ0/y8MMPk5WVxd13311o/y21QzanXlML5jvvvJN+/frx9ttvl5kNs4eHB1999dUNNsyvvvoq/fv3L1MbZluL/P9pmvYYsBt4SUTi8xfQNG0UMAqgRo0aREREWN7KSeBzOP/NeVhfTNlo/SUmPYaYiBjz6m8M+MLeCXthciFl0oGLcMHtAhciLphXrwCecCz8GMdaHjPvHJsxENjEn3++DfQpcS3Jyckl+x2WEu3ZZ2ns4kJKo0ac69gRyqgPfn5+Rs+WjIwMRMSqI8WMjIxCvWlA94T55ptv2LJlC1lZWdx22220bt2apKQkMjMzMRgMnDp1ipUrV7Jnzx40TSMhIQF/f3/69OnD3XffzeDBgwEYNGgQM2fOpHv37kyZMoXXX3+d6dOnk52dzcWLF1m3bh0Ahw4dynPegAED+PDDD2ncuDG7du3ihRdeYP369YwZM4YRI0bw8MMPs3DhQoACr+Wjjz4y2iGHhobSu3dvox1ymzZtmDBhAhMnTmTu3LmMHz/erHrfeOMN9u/fz59//gnAn3/+yc6dO9m+fTtBQUEkJSVZ3O6kSZP48ccfqV27NgkJCSQlJZGamkpWVhZJSUnUqVOH9evX4+bmxqZNmxg/fjxLly7ltdde459//uH9998H4K233qJr16589NFHJCQkEBYWRufOnVm0aBHu7u789ddfREVFcdttt5GSknLDtaWlpVn2N1bYVFhzNmAjEFXANgioAbiih4SmAl8WV19JbQ2y07MlnHAJJ1wyLmcUWfbC8gsSTrgk7U+yqI0Tr52QcC1cUo6mFHg8cXuihBMuF1detKjenbfslP3991t0jq2Ij98iOTk5parD6rYGDo69bQ0+/PBDmThxovHnF154QWZet2wYPny4/PDDD5KZmSnBwcHyxBNPyMqVKyU9PT3PcRGRhIQEqVevnrGe48ePS9u2bUVEpGfPnhIREWE8ZnpeUlKSeHl5SZs2bYxb06ZNRUSkWrVqkpGh/z0mJiZKpUqVCryGt956S4KDgyU4OFiqVKki27ZtExERDw8P4/tx+fLl8uSTT5pd78mTJ6VVq1bGn8PDwyU0NLRU7Y4ePVp69eolCxculMuXLxvr7devn4iInD59WgYPHiytWrWSli1bSrNmzUREZNGiRTJmzBhju+3bt5dWrVoZ71e9evXk0KFDMmjQINm0aZOxXNu2bWXXrl03XJultgalGsmLSOEBPxM0TfsMWFeatorCxeO/RwtnPz1L0BtBhZbNnQjlfbP5YRWAOs/WIe6DOE5NPkWLr1vccPzarmsA+HayzEvcu4k3qYcc44FnrgWxSDaaZr/8fYVlFDfPwc3NjZ07d7Jp0yaWL1/O3Llz+eOPPyxqw9TC2JScnBz8/f2JjIw07jMdeRbXN1M7ZB8fH0JDQ4u1Qzan3uKuoSTtzp8/nx07drB+/XpCQkLyXDP8Z8O8atUqoqKiCn0gK9dtmJs1a3bDMVvMWbFldk0tkx/vQR/h24yqvfUl9c5+fJbstMK/LhuOG/Co41Fs+mR+PGt6UmdsHS4su0Dy/uQbjiftTIJq4FnHslWlvBt7Y4ixbxqlKf/++xU7d7YsX5k2FZgePXqwatUqDAYDSUlJrF279oYyycnJJCYm0rdvX2bPnm0UJ1N7YD8/P6pWrWoMb3z99df07NmzwDZNz6tSpQoNGzbkhx9+AHQBO3DgAKAvQLJ8+XIAo71wfkpih2xOvUVZMJe0XUtsmE37ZW8bZltm18zQNO2Apmn7gTDApguL5j5IzbyYycVlFwstlxKVgk/zkj1YrP9Kfdz83Ih57cZY/rWd16C55Z/E3k28kQz7plGa4uXVEIPhaPnLtKmgtGvXjgcffJCQkBCGDBnCbbfduCBMUlIS/fv3Jzg4mJ49e/Lhhx8CMHToUGbOnEnbtm05ceIES5Ys4eWXXyY4OJjIyEjefPPNAtvMf96yZcv44osvaNOmDa1atWL9ev3B2EcffcQnn3xCx44dSUxMLLCuktghm1NvURbMJW3XGjbM3333XdnbMBcWx7HHVhqr4dMfnpZwwuXv+n/LtkbbJDs9+4YyWYYsiXCLkBMTTpS4nVPTTt1gE5wWl6Y/E3gi3OL6roZflXDC5crvV0rcJ2uzd+8dsnVrDcnKKvj5Q1GomLyy2VX3QFkN24TcdMSaj9UkLSaN85+fv6FMyr4UJEssjpubUufZOnjW9eTwo4dJP6dbAcTNjtMfMZv1hKLgfts7V96Ucps3r1AobsBpRD43XOPd2Bu/Hn7ETo4lKzkrT5lrO68/HC3EYtgcXH1caf1TazKvZHKg3wGubLjCufnnuOmBm6BW8efnx6OWBy7eLkbTNEfANG8+J6dkyyAqFArHwGlE3quhF7jqD1YbTWtE5oXMG2x8r227hkctD4sfjubHt50vrX5oRfKBZA70O4DkCPUn1C9RXZqLpj98daCRPEDjxh8QHLweFxcPe3dFoVCUAqeY8Qrg4u6CVwMvUo+l0rBrQwIGBXB62mkC+gXg29aX9PPpXFp5iZqP17RKmlJAnwA67utI5tVMfFr44BHoARElq8u7sTephx0rm6Vy5WB7d0GhUFgBpxnJw3VXx+thj6bzmuIe4M6BvgcwxBj0hbOzhHrj6lmtvUqtKuF/m78u8KXAu4ljpVHmkpOTzuHDI4iLm2PvrigUihLiXCJ/PewhInjW9iR4QzDZhmx23LyDuA/iqH5/dXwaO54vi3fj62mUpx0jjTIXFxdP0tPjOHXqXZU3r1CUU5xK5H2a+JB9LZvMy5mAPtIOCQ+hwcQGNPuiGc2/aG7nHhaM0Y3SgR6+5qIybZyHd99916rlTFm8eDH/l2sQZ0UKqzciIoK///7b6u3lZ8SIEaxYsaLIMosXL+bcuXMW1RsbG0vr1q1L0zWzcSqRz7UPNhVL37a+NJzckFpP1MK1kmNO1TemUTqgyJtm2qjRvGMjIuTk5BR63JYiX9YUJfKm9gdlQUlEvixxmgevkNef3a9rEZbADoYxjdLBMmxyCQp6i8jIHpw7N5969V60d3cclueff549e6xsNRwSwuzZhVsNx8bG0qdPH8LCwti2bRurV6/m77//5t1330VE6NevH9OnT2fChAkYDAZCQkJo1aoVy5YtK9Bqt6ByS5cuZc6cOWRkZNC5c2fmzZuHq6srixYt4r333qNWrVo0bdoUT88bs9ZM7Xe9vb1ZtGgRzZo1Y/HixaxZs4bU1FROnDjBPffcw4wZMwCKrTc2Npb58+fj6urK0qVL+fjjj/niiy+oVq0ae/fuNc4CtqTd7OxsnnzySXbv3o2maTzxxBO88ELeSfqTJ09m7dq1GAwGbr31VhYsWMDKlSvZvXs3w4YNw9vbm23btnHo0CHGjh2LwWAgMDCQxYsXU6tWLfbs2cMTTzyBj48P3bt3t8K7w0wKmyVlj600M15FrrtRuoRLzMSYUtVTUkoz29OR3CgL4syZ2WIwnC62XEWe8Tp27Fjp3r279OzZ02rb2LFji2z/5MmTomma0UHx7NmzUq9ePbl48aJkZmZKWFiYrFq1SkTkBrfGK1f0WdapqanSqlUro7OiablDhw5J//79ja6PTz/9tCxZskTOnTtnbCc9PV1uvfVWo9Oi6WzPxMREyczMFBGR33//Xe69914R0Z0ZGzZsKAkJCWIwGKR+/fpy+vTpIus15a233jK6bYrozpj9+vWTrKysErW7e/du6dWrl7G++Ph4Y725jpu590tE5JFHHpE1a9aIiO7SmesWmZGRIV27dpWYGF2Dli9fLo8//riIiNxyyy1GN89x48blccm0hDJ1oXQ0XDxc8AryctgRcVE4YhqlKXXrjrV3Fxye2bNnk5SUhK9vySfblYQGDRoYvVd27dpFaGgo1atXB2DYsGFs2bLF6P1uypw5c1i1ahUAZ86c4dixYwQEBOQps2nTJvbs2UPHjh0BMBgM3HTTTezYsSNPOw8++CBHjx69oY3ExESGDx/OsWPH0DSNzMxM47E77rgDPz/9G3fLli05deoUly9fNqvegrj//vuN36IsbbdVq1bExMTw7LPP0q9fP3r37n1D/eHh4cyYMYPU1FSuXr1Kq1atGDBgQJ4y0dHRREVFMWjQIFxcXMjOzqZWrVokJiaSkJBgNH179NFH+fnnn826rtLiVDF5uC6WxxxXLAvDERb1Lo6kpEgOHnxAxeYdDFMLXRHz3j+mVrv79u2jbdu2RqtdU0SE4cOHExkZSWRkJNHR0UyaNAkwz4wv1343KiqKtWvX5mnDNAxjaulb0nkspvfB0narVq3Kvn37CA0N5ZNPPmHkyJF56k5LS+OZZ55hxYoVHDhwgP/973+F3q9WrVrx119/ERkZyYEDB/jtt98QEZvYCJuD84l8k//SKMsTjrCod3FkZydx6dIPKtPGgencuTObN2/m8uXLZGdn8+233xpHj+7u7sYRbVFWu6bl7rjjDlasWMHFi7qz69WrVzl16hSdO3cmIiKCK1eukJmZabQazo+p/e7ixYvN6r859ZpjJWxJu5cvXyYnJ4chQ4bwzjvv8M8//+Q5nivogYGBJCcn58m4Me1Ls2bNuHTpEjt27AAgMzOTgwcP4u/vj5+fH1u3bgUKt0i2Bc4n8o299TTKS5nFF3YgHGVR76JQmTaOT61atXjvvfcICwujTZs2tGvXjkGDBgEwatQogoODGTZsWJFWu6blWrZsyZQpU+jduzfBwcHceeednD9/nlq1ajFp0iS6du1Kr169aNeuXYH9Kcx+t6j+m1PvgAEDWLVqFSEhIUYP/NK0e/bsWUJDQwkJCWHEiBG89957eY77+/vzv//9j1tuuYXBgwcbw1egp1k+9dRThISEkJ2dzYoVK3jrrbdo06YNISEhxiygRYsWMWbMGLp27Yq3t2WLFpWKwoL19thK++BVROTyussSTrgk/JVQ6rospTQPHQ1nDBJOuMR9Eme9DtmA+PgtEh6OnD79QYHHK/KDVxFlsyui7oGIshq2KeVhRFwQnrU99TTKE47dbzWaVyjKF06VXQPgFeQFLo45sagoHNWNsiAaNnyHhITN9u6GQqEwA6cT+dw0ynKZYePgaZS5+Pl1xc+vq727oVAozMDpwjVw3aisnI3koXykUeYiIly48C0XLpRdloBCobAc5xT58p5G6WBulAWhaRrnz3/B8eMvqdi8QuHAOKXI53ejLC84shtlQSiHSoXC8XFKkTe6UZaDh5imOOKi3kWhMm3KF3/++SetWrUiJCSEw4cPl9jq1tGtdRV5cU6RL6dplB61HW9R7+JQo3nHQoqwG162bBnjxo0jMjKyVJNxHN1ahzpXrAAAEvtJREFUV5GXUmXXaJp2PzAJaAF0EpHdJsdeBZ4EsoHnROTX0rRlCV5B/y3qXZ7QNK3cPTT297+NWrX+x840P27bto3TQP1t25jaqBHDatSwd/fKlGPPHyNxT6JVrYYrh1SmyewmRZbJbzf8/PPPM3/+fNLT07n55ptZtGgRy5cv5/vvv+fXX39l48aNTJ061Xh+dnY2EyZMICIigvT0dMaMGcPo0aMBmDFjBl9//TUuLi706dOHDh06FGit++KLL5KcnExgYCBz587F19fXfta6ijyUdiQfBdwLbDHdqWlaS2Ao0Aq4G5inaVqZrdjh4qEv6l2exDKX3IfG5Ynd/u/w2PnGnEpPR4BT6emMio5m2YUL9u5ahSE6OprHHnuM33//nS+++IKNGzfyzz//0KFDBz744ANGjhzJwIEDmTlz5g2+KV988QV+fn7s2rWLXbt28dlnn3Hy5El+/vlnVq9ezY4dO9i3bx/jx4/nvvvuo0OHDixbtozIyEjc3Nx49tlnWbFihVHUJ0+eDMDjjz/OnDlz2LZtmz1uieI6pRrJi8hhKNA1bhCwXETSgZOaph0HOgFl9ts2XdS7POHd2Jsr664g2YLmah/XOkt5PSaGnJwU7mMdaxlAOl6k5uTwekxMhRrNN5ndxC5Ww/Cf3fC6des4dOgQ3bp1AyAjI4OuXYue0/Dbb7+xf/9+o+lWYmIix44dY+PGjTz++OP4+OjPiqpVq3bDubnWunfeeSegfyuoXr26Xa11FXmx1WSoOsB2k5/jru+7AU3TRgGjAGrUqEFERIR1euANHIaI8AgoI61MTk4uff+zgQzY/P1mqGWNXtme00AzYknCl0zc/9ufnm6936eD4ufnl8cNMTs7u0h3RFuQnJyMt7c3SUlJpKamEhoayqJFi/KUSUpKIjMzE4PBQFJSEsnJyeTk5Bj3T58+nV69euU5Z82aNaSnp99wPdnZ2aSkpBjrad68OZs2bcpz/Nq1a8Z2AVJSUoztVQRs+T5IS0uz6O+qWJHXNG0jULOAQ6+LyE+FnVbAvgKT1kVkIbAQoEOHDhIaGlpcl8ziTOQZTqw+wa2tb8WjuodV6iyOiIgIStv/eOLZ9/4+ggODqRZ648jJEam/bRtH0ltwhBZ593t6ElrMKLK8c/jw4Twjd3uM5CtXroyLiwu+vr6EhYUxbtw4Lly4QOPGjUlNTSUuLo6mTZvi7u6Ot7c3vr6+ec7p168fS5YsoX///ri7u3P06FHq1KlD//79mTx5sjGufvXqVapVq4a/vz85OTn4+vrSrl07rl69SlRUFF27diUzM5O9e/fSqVMn/P392bdvH927d2f16tXG9ioCtnwfeHl50bZtW7PLFxuTF5FeItK6gK0wgQd95F7P5Oe6QJk+jnfkxbGLojz2e2qjRvi45H0r+bi4MLVRIzv1qOJSvXp1Fi9ezEMPPURwcDBdunThyJEjRZ4zcuRIWrZsSbt27WjdujWjR48mKyuLu+++m4EDB9KhQwdCQkKYNWsWULC17iuvvGK01s31Urebta4iL4XZU1qyARFAB5OfWwH7AE+gIRADuBZXjzWshnNJOZIi4YTL+SXnrVZncVjDZjcnO0c2e2+WYy8cK32HypCl//4rDf7+W7TwcGnw99+y9N9/7d2lMkFZDd+IugdOZDWsado9mqbFAV2B9Zqm/Xr9g+Mg8D1wCPgFGCMixTv3WxGvhtfdKMtZporRjbIcjeQBhtWoQWzXrvwBxHbtWqEeuCoUjkxps2tWAasKOTYVmFrQsbLAuKh3ORNLKD9ulAqFwvFxOqthUxx9UW/JEdLPpmM4bsBwzGB8TfwrkaxrWXZd/FehUDgHzi3yTby5tuOaXcXyBiE/ZiD1WCqG4wbSTqSRk/bfFHTNU8O7kTdVulbBP9RfCbxCoSg1Ti3yPk18yE7U3ShtmUaZK+TshXNHzxkFPfVYasFCfrM33o29qXZ3Nbwbe+PTxAfvJt541vEsNxOgFApF+cCpRd7UjbK0Im8ckZuEVQzHbxTyoxw1jsi9m+QT8sbeeNZVQq5QKMoO5xZ5E392v1v9ii1fWIzccNyA4YSBHEPhI3KfJj4cTT5Kl/u6KCFXFMqyCxd4PSaG0+np1Pf0tIuR28iRI3nxxRdp2bJloWVWr15N06ZNiyyjKB84tcgbF/U2SaO8YURuZmil6l1V8W7sbRyVFyTkRyOO4tXAq8yuT1G+WHbhAqOio0m9bgWca+QGlKnQf/7558WWWb16Nf3791ci7wQ4tcjnplFeWnGJlKgUo6jnEXKP60KuQisKG/N6TIxR4HOxhpFbbGwsd999N507d2bv3r00bdqUr776im3btjFu3DiysrLo2LEjn376KZ6enoSGhjJr1iw6dOhA5cqVGTt2LOvWrcPb25uffvqJEydOsGbNGjZv3syUKVNYuXIl69evZ/78+bi5udGyZUuWL19e2tuhKCOcWuQB/Hv4c+HbC6Dp4Zuqvavi3aToEblCYQtOp6dbtN8SoqOj+eKLL+jWrRtPPPEEH3zwAQsWLGDTpk00bdqUxx57jE8//ZTnn38+z3kpKSl06dKFqVOnMn78eD777DPeeOMNBg4cSP/+/bnvvvsAmDZtGidPnsTT05OEhIRS91dRdjjlylCmNF/UnB6pPeh0qBO3/HQLjd9vTJ2n6lCtVzW8GngpgVeUGfU9PS3abwn16tUz2gs/8sgjbNq0iYYNG9K0aVMAhg8fzpYtW244z8PDg/79+wPQvn17YmNjC6w/ODiYYcOGsXTpUtzcnH5s6FQ4vciDbhWgUNgbWxq5lXROhbu7u/FcV1dXsrKyCiy3fv16xowZw549e2jfvn2h5RSOR4UQeYXCERhWowYLmzWjgacnGtDA05OFzZpZ5aHr6dOnjSswffvtt/Tq1YvY2FiOHz8OwNf/3979x1R1ngEc/z69094tGNTgnK3t0JZZRZCrtMV2mVK0OP+o00qi2VqNppVmaafZ2tSx6DT6h7Vx6mLbNJ1C20Ux1DKTuYxqIdofqysCCiIqXZ0/SFUUqiXVgs/+uMc7EJAfl8uFc59PQrz3fc85930fuQ/nvufc933nncACHp0xaNCgwHzoN27c4PTp06SmpvLKK69QV1fH1atXg26z6R32ucuYXvTL4cNDcifN2LFjycnJYcmSJcTFxbFp0yZSUlLIyMgIXHjNzMzs9PHmzZvHM888w+bNm9mxYweLFy+mvr4eVWXZsmUMHjy4x/tgQsOSvDEucMcdd/DGG2+0KEtLS6OkpKTVts1XFWp+Rj537tzAhdZHH32Uo0ePBuo++uijHm6x6S02XGOMMS5mSd6Yfi42Npby8vJwN8P0UZbkjTHGxSzJG2OMi1mSN8YYF7Mkb4wxLmZJ3ph+LioqCoBz584FboEMlWPHjpGUlITP56O6urpHjnGz/V2Vn5/f4jbPFStWsHfv3m4dy80syRvjEnfddRd5eXkhfY38/HxmzZpFSUkJ9913X7vbNTU1BX2MzrSleZJfvXo106ZN6/bx3MqSvDE9qKpqJiUlU1v8nD37GgBNTQ2t6kpKplJTkw3A9esXW9V1xZdffsn48eMByM7OZs6cOcyYMYO4uDheeumlwHYFBQVMnjyZiRMnkpGR0eYUBaWlpaSkpJCYmMjs2bO5fPkye/bsYePGjbz11lukpqa22icqKooVK1aQmprKp59+SnFxMVOmTGHSpEmkp6dTU1PT4THWr1/Pgw8+SGJiIitXrgyUv/322yQmJjJhwgSeeuopPvnkE3bv3s2LL75IUlIS1dXVLFy4MPBHbt++ffh8PhISEli0aBHXnJk+Y2NjWblyJRMnTiQhIYFjx451Kcb9kSV5Y1yqtLSU3Nxcjhw5Qm5uLqdPn+bixYusWbOGvXv3cujQIZKTk9mwYUOrfZ9++mnWrVvH4cOHSUhIYNWqVcycOZPMzEyWLVtGYWFhq32++eYbxo8fT2FhIQ8//DDPP/88eXl5FBcXs2jRIrKysm57jIKCAk6cOMHBgwcpLS2luLiY/fv3U1FRwdq1a/nwww8pKytj06ZNPPLIIzzxxBOsX7+e0tLSFp8Ivv32WxYuXBjoe2NjI6+//nqgPiYmhkOHDvHcc8/x6quv9mDE+yab1sCYHjRmzB4GDRrUZp3H8wN8vqJ29x04MOa29V2VlpZGdLR/2ctx48Zx6tQp6urqOHr0aGBa4uvXrzN58uQW+9XX11NXVxeY0GzBggVkZGR0+Hoej4cnn3yShoYGqqqqKC8vZ/r06YB/+GbEiBG33b+goICCggJ8Ph/gn3LhxIkTlJWVMXfuXGJiYgAYOnTobY9TVVXVaprlLVu2BObSnzNnDuCfWnnXrl0d9qu/CyrJi0gG8EdgLPCQqn7ulMcClUCVs+m/VLXzsyMZY4J2Z7N56m9OI6yqTJ8+ne3bt/f463m9XjweDwCqSnx8fGBmzM5QVZYvX86SJUtalG/evLlLUymr6m3rb8bldlMru0mwwzXlwByg9WoEUK2qSc6PJXhj+oCUlBQ+/vjjwBTEDQ0NHD9+vMU20dHRDBkyhAMHDgBdn6YYYMyYMVy4cCGQ5L/77jsqKipuu096ejpbt24NXCM4e/Ys58+fJy0tjZ07d1JbWwvApUuXgJbTITf3wAMPBDXNstsEdSavqpXQ/QULjDG9a9iwYWRnZzN//vzAxcg1a9YEhjZuysnJITMzk4aGBkaPHs22bdu69DoDBw4kLy+PF154gfr6ehobG1m6dCnx8fHt7vP4449TWVkZGD6Kiori3XffJT4+nqysLKZMmYLH48Hn85Gdnd1iOuTmdxV5vV62bdvW7WmW3UY6+mjTqYOIFAG/u2W4pgI4DnwN/EFVD7Sz77PAswDDhw+f1J8XCL569Wq37/l1i0iLQXR0NPfff3/geVNTU2DIIlJZDEIbg5MnT1JfX9+iLDU1tVhVk9vavsMzeRHZC/yojaosVf1bO7vVAPeqaq2ITALyRSReVb++dUNVfRN4EyA5OVmnTp3aUZP6rKKiIvpz+3tCpMWgsrKyxYXWK1eutHvhNVJYDEIbA6/XG7g43RkdJnlV7fK3C1T1GnDNeVwsItXAT4DPu3osY4wx3ReS++RFZJiIeJzHo4E44ItQvJYx4dYTQ57GdEZ3fteCSvIiMltEzgCTgb+LyD+dqp8Bh0WkDMgDMlX1UjCvZUxf5PV6qa2ttURvQk5Vqa2txev1dmm/YO+ueR94v43y94D3gjm2Mf3ByJEjOXPmDBcuXAD837bs6pvQbSwGoYuB1+tl5MiRXdrHvvFqTBAGDBjAqFGjAs+Lioq6dFHMjSwGfSsGNneNMca4mCV5Y4xxMUvyxhjjYj3yjdeeIiIXgFPhbkcQYoCL4W5EmEV6DCK9/2AxgN6PwY9VdVhbFX0qyfd3IvJ5e18tjhSRHoNI7z9YDKBvxcCGa4wxxsUsyRtjjItZku9Zb4a7AX1ApMcg0vsPFgPoQzGwMXljjHExO5M3xhgXsyRvjDEuZkk+SCKSISIVInJDRJJvqVsuIidFpEpE0sPVxt4gIjOcfp4UkZfD3Z7eICJbReS8iJQ3KxsqIh+IyAnn3yHhbGMoicg9IlIoIpXOe+A3TnkkxcArIgdFpMyJwSqnfJSIfObEIFdEBoarjZbkg9fmYuYiMg6YB8QDM4DXbs6x7zZOv7YAPwfGAfOd/rtdNv7/2+ZeBvapahywz3nuVo3Ab1V1LJAC/Nr5f4+kGFwDHlPVCUASMENEUoB1wJ+cGFwGFoergZbkg6Sqlapa1UbVLGCHql5T1f8AJ4GHerd1veYh4KSqfqGq14Ed+Pvvaqq6H7h1nYRZQI7zOAf4Ra82qhepao2qHnIeXwEqgbuJrBioql51ng5wfhR4DP9aGhDmGFiSD527gdPNnp9xytwokvrakeGqWgP+JAj8MMzt6RUiEgv4gM+IsBiIiEdESoHzwAdANVCnqo3OJmF9P9h88p3QzcXMpY0yt96vGkl9NbcQkSj8iwQtVdWvRdr6dXAvVW0CkkRkMP5FlMa2tVnvtur/LMl3QncWM8f/1/ueZs9HAud6pkV9TiT1tSNficgIVa0RkRH4z+5cS0QG4E/wf1XVXU5xRMXgJlWtE5Ei/NcnBovI95yz+bC+H2y4JnR2A/NE5E4RGYV/MfODYW5TqPwbiHPuKBiI/4Lz7jC3KVx2AwucxwuA9j7p9XviP2X/C1CpqhuaVUVSDIY5Z/CIyPeBafivTRQCc53NwhoD+8ZrkERkNvBnYBhQB5SqarpTlwUswn8XwlJV/UfYGhpiIjIT2Ah4gK2qujbMTQo5EdkOTMU/rexXwEogH9gJ3Av8F8hw6yL2IvJT4ABwBLjhFP8e/7h8pMQgEf+FVQ/+k+adqrpaREbjvwFhKFAC/EpVr4WljZbkjTHGvWy4xhhjXMySvDHGuJgleWOMcTFL8sYY42KW5I0xxsUsyRtjjItZkjfGGBf7HwE8YtdxJ55VAAAAAElFTkSuQmCC\n", "text/plain": [ "
" ] @@ -1132,7 +1132,7 @@ ], "source": [ "# create custom segment\n", - "sw_full_segment = SineWaveSegmentFull([0, 0], [5, 5], 4, 1)\n", + "sw_full_segment = SineWaveSegmentFull([0, 0], [5, 5], 4, 2)\n", "\n", "# create a shape\n", "shape_12 = geo.Shape(sw_full_segment)\n", @@ -1165,11 +1165,11 @@ "data_shape_16 = shape_16.rasterize(0.25)\n", "\n", "# plot data\n", - "plt.plot(data_shape_12[0], data_shape_12[1], 'rx', label=\"original\")\n", - "plt.plot(data_shape_13[0], data_shape_13[1], 'bx', label=\"translated\")\n", - "plt.plot(data_shape_14[0], data_shape_14[1], 'gx', label=\"distorted and translated\")\n", - "plt.plot(data_shape_15[0], data_shape_15[1], 'kx', label=\"rotated and translated\")\n", - "plt.plot(data_shape_16[0], data_shape_16[1], 'mx', label=\"reflected\")\n", + "plt.plot(data_shape_12[0], data_shape_12[1], 'r', label=\"original\")\n", + "plt.plot(data_shape_13[0], data_shape_13[1], 'b', label=\"translated\")\n", + "plt.plot(data_shape_14[0], data_shape_14[1], 'g', label=\"distorted and translated\")\n", + "plt.plot(data_shape_15[0], data_shape_15[1], 'k', label=\"rotated and translated\")\n", + "plt.plot(data_shape_16[0], data_shape_16[1], 'm', label=\"reflected\")\n", "plt.plot([point_0[0], point_1[0]], [point_0[1], point_1[1]], 'co', label=\"points\")\n", "plt.plot([point_0[0], point_1[0]], [point_0[1], point_1[1]], 'y--', label=\"line of reflection\")\n", "plt.grid()\n", From ab6513ba863940f058f190b402b4596f37f6af41 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Mon, 10 Feb 2020 14:28:06 +0100 Subject: [PATCH 160/177] Correct doc strings --- mypackage/geometry.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mypackage/geometry.py b/mypackage/geometry.py index 7d2d4e3..be39b15 100644 --- a/mypackage/geometry.py +++ b/mypackage/geometry.py @@ -671,7 +671,7 @@ def apply_reflection_across_line(self, point_start, point_end): Apply a reflection across a line. :param point_start: Line of reflection's start point - :param point_start: Line of reflection's end point + :param point_end: Line of reflection's end point :return: --- """ point_start = ut.to_float_array(point_start) @@ -746,7 +746,7 @@ def reflect_across_line(self, point_start, point_end): Get a reflected copy across a line. :param point_start: Line of reflection's start point - :param point_start: Line of reflection's end point + :param point_end: Line of reflection's end point :return """ new_shape = copy.deepcopy(self) From cc51afbbdaf96ca891db8603d53dffc91e957897 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Mon, 10 Feb 2020 14:38:04 +0100 Subject: [PATCH 161/177] Add exception if empty shape is rasterized --- mypackage/geometry.py | 2 ++ tests/test_geometry.py | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/mypackage/geometry.py b/mypackage/geometry.py index be39b15..0a4fcc5 100644 --- a/mypackage/geometry.py +++ b/mypackage/geometry.py @@ -715,6 +715,8 @@ def rasterize(self, raster_width): :param raster_width: The desired distance between two raster points :return: Array of contour points (3d) """ + if self.num_segments == 0: + raise Exception("Can't rasterize empty shape.") if not raster_width > 0: raise ValueError("'raster_width' must be > 0") diff --git a/tests/test_geometry.py b/tests/test_geometry.py index 37a653b..224e0db 100644 --- a/tests/test_geometry.py +++ b/tests/test_geometry.py @@ -881,6 +881,10 @@ def test_shape_rasterization(): shape.rasterize(0) with pytest.raises(Exception): shape.rasterize(-3) + # empty shape + shape_empty = geo.Shape() + with pytest.raises(Exception): + shape_empty.rasterize(0.2) def default_test_shape(): From 4c82a1320c0a849aaff23043fb89612417cb7e38 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Mon, 10 Feb 2020 16:10:07 +0100 Subject: [PATCH 162/177] Add visualization package --- codecov.yml | 1 + mypackage/visualization.py | 53 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 mypackage/visualization.py diff --git a/codecov.yml b/codecov.yml index c371f9b..cbbb0e3 100644 --- a/codecov.yml +++ b/codecov.yml @@ -25,6 +25,7 @@ coverage: ignore: - "tests" - "*__init__.py" + - "*visualization.py" #comment: #layout: "reach, diff, flags, files" diff --git a/mypackage/visualization.py b/mypackage/visualization.py new file mode 100644 index 0000000..c150879 --- /dev/null +++ b/mypackage/visualization.py @@ -0,0 +1,53 @@ +"""Contains some functions to help with visualization.""" +import matplotlib.pyplot as plt +import numpy as np + + +def plot_coordinate_system(coordinate_system, axes): + """ + Plot a coordinate system in a matplotlib 3d plot. + + :param coordinate_system: Coordinate system + :param axes: Matplotlib axes object (output from plt.gca()) + :return: --- + """ + p0 = coordinate_system.origin + px = p0 + coordinate_system.orientation[:, 0] + py = p0 + coordinate_system.orientation[:, 1] + pz = p0 + coordinate_system.orientation[:, 2] + + axes.plot([p0[0], px[0]], [p0[1], px[1]], [p0[2], px[2]], 'r') + axes.plot([p0[0], py[0]], [p0[1], py[1]], [p0[2], py[2]], 'g') + axes.plot([p0[0], pz[0]], [p0[1], pz[1]], [p0[2], pz[2]], 'b') + + +def set_axes_equal(axes): + """ + Adjust axis in a 3d plot to be equally scaled. + + Source code taken from the stackoverflow answer of 'karlo' in the + following question: + https://stackoverflow.com/questions/13685386/matplotlib-equal-unit + -length-with-equal-aspect-ratio-z-axis-is-not-equal-to + + :param axes: Matplotlib axes object (output from plt.gca()) + """ + + x_limits = axes.get_xlim3d() + y_limits = axes.get_ylim3d() + z_limits = axes.get_zlim3d() + + x_range = abs(x_limits[1] - x_limits[0]) + x_middle = np.mean(x_limits) + y_range = abs(y_limits[1] - y_limits[0]) + y_middle = np.mean(y_limits) + z_range = abs(z_limits[1] - z_limits[0]) + z_middle = np.mean(z_limits) + + # The plot bounding box is a sphere in the sense of the infinity + # norm, hence I call half the max range the plot radius. + plot_radius = 0.5 * max([x_range, y_range, z_range]) + + axes.set_xlim3d([x_middle - plot_radius, x_middle + plot_radius]) + axes.set_ylim3d([y_middle - plot_radius, y_middle + plot_radius]) + axes.set_zlim3d([z_middle - plot_radius, z_middle + plot_radius]) From 67a19281e3cc25b603b181459245973488afc9ee Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Mon, 10 Feb 2020 16:11:43 +0100 Subject: [PATCH 163/177] Fix flake8 and pydocstyle issues --- mypackage/visualization.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mypackage/visualization.py b/mypackage/visualization.py index c150879..78f41e7 100644 --- a/mypackage/visualization.py +++ b/mypackage/visualization.py @@ -1,5 +1,5 @@ """Contains some functions to help with visualization.""" -import matplotlib.pyplot as plt + import numpy as np @@ -32,7 +32,6 @@ def set_axes_equal(axes): :param axes: Matplotlib axes object (output from plt.gca()) """ - x_limits = axes.get_xlim3d() y_limits = axes.get_ylim3d() z_limits = axes.get_zlim3d() From 1ef418b1e63118705160e20b4650c42ee58a367c Mon Sep 17 00:00:00 2001 From: mnagel Date: Mon, 10 Feb 2020 16:29:01 +0100 Subject: [PATCH 164/177] U-groove and V-groove as functions * added U and V groove as functions * input is a dictionary * output is a profile --- Weld_tester.ipynb | 563 +++++++++++++++++++++++++++++++++++++++ mypackage/all_groove.py | 120 +++++++++ mypackage/mark_funcs.py | 37 --- tests/test_mark_funcs.py | 9 - 4 files changed, 683 insertions(+), 46 deletions(-) create mode 100644 Weld_tester.ipynb create mode 100644 mypackage/all_groove.py delete mode 100644 mypackage/mark_funcs.py delete mode 100644 tests/test_mark_funcs.py diff --git a/Weld_tester.ipynb b/Weld_tester.ipynb new file mode 100644 index 0000000..fd14836 --- /dev/null +++ b/Weld_tester.ipynb @@ -0,0 +1,563 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import matplotlib.pyplot as plt\n", + "from astropy.units import Quantity\n", + "import numpy as np\n", + "import copy\n", + "\n", + "import mypackage.geometry as geo\n", + "import mypackage.point_cloud_generator as pcg\n", + "import mypackage.transformations as tr" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[[2. 1.86666667 1.73333333 1.6 1.46666667 1.33333333\n", + " 1.2 1.06666667 0.93333333 0.8 0.66666667 0.53333333\n", + " 0.4 0.26666667 0.13333333 0. ]\n", + " [0. 0.06666667 0.13333333 0.2 0.26666667 0.33333333\n", + " 0.4 0.46666667 0.53333333 0.6 0.66666667 0.73333333\n", + " 0.8 0.86666667 0.93333333 1. ]]\n" + ] + }, + { + "data": { + "text/plain": [ + "[]" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "seg = geo.LineSegment([[2,0],[0,1]])\n", + "seg_data = seg.rasterize(0.15)\n", + "print(seg_data)\n", + "plt.plot(seg_data[0], seg_data[1])" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'t': ,\n", + " 'alpha': ,\n", + " 'b': ,\n", + " 'c': }" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Values for the Single V Groove Butt Weld\n", + "# in a dictionary\n", + "t = Quantity(0.009,unit=\"meter\")\n", + "alpha = Quantity(40, unit=\"deg\")\n", + "b = Quantity(0.2, unit=\"centimeter\")\n", + "c = Quantity(1, unit=\"millimeter\")\n", + "v_naht_dict = dict(t=t, alpha=alpha, b=b, c=c)\n", + "width_default = Quantity(5, unit=\"millimeter\")\n", + "v_naht_dict" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "# V-naht berechnungen und erstellen der einzelnen Teilsegmente\n", + "bottom = geo.LineSegment([[-width_default.to_value(\"millimeter\"),0],[0,0]])\n", + "root_face = geo.LineSegment([[0,0],[0,v_naht_dict[\"b\"].to_value(\"millimeter\")]])\n", + "alpha_halbe = v_naht_dict[\"alpha\"].to_value(\"deg\")/2\n", + "s = np.tan(alpha_halbe)\n", + "groove_face = geo.LineSegment([[0,-s],[v_naht_dict[\"b\"].to_value(\"millimeter\"),\n", + " v_naht_dict[\"t\"].to_value(\"millimeter\")]])\n", + "top = geo.LineSegment([[-s, -width_default.to_value(\"millimeter\")],\n", + " [v_naht_dict[\"t\"].to_value(\"millimeter\"),\n", + " v_naht_dict[\"t\"].to_value(\"millimeter\")]])" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[]" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAWoAAAD4CAYAAADFAawfAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4xLjEsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy8QZhcZAAASP0lEQVR4nO3de4yddZ3H8fe3Hcq1CKQ1aAu2gixUVsQMLLdFuUhrgbbzJLthEwzuJkvWuIrG1RVNdrP/bdS4bvaWEGETI9GohVJapMhF5Q4FilIKishVLgPIbbmUtt/945lBGKad03ae83vOnPcrITNzzunMJ/TMp8/5nt/z/CIzkSS117TSASRJ22ZRS1LLWdSS1HIWtSS1nEUtSS030MQ3nTVrVs6bN6+Jby1JU9Idd9zxTGbOHu++Rop63rx5rF27tolvLUlTUkQ8vLX7HH1IUstZ1JLUcha1JLWcRS1JLWdRS1LLWdSS1HIWtSS1XCPrqNXDvv992LChdIrumjYNPvUp8CQttZRFrbf70Y9gxYrSKborEx55BC66qHQSaVwWtd7ukktKJ+i+s8+GlSth0yYY8FdC7eOMWqoqePZZuP760kmkcVnU0sKFsPvu/flqQj3Bopb23LMu60svhS1bSqeR3sGilqAefzz+OHjVR7WQRS0BnHFG/Uai4w+1kEUtAey7L5x8MixfXi/Xk1rEopZGVRU88ACsX186ifQ2FrU0aulSiHD8odaxqKVR++8Pxx1nUat1LGrpraoK7r4bHnywdBLpTRa19FZDQ/XHSy8tm0N6C4taeqv58+HIIx1/qFUsammsqoKbboInniidRAIsaumdHH+oZSxqaawFC+CQQyxqtYZFLY0VUY8/rrsOnnuudBrJopbGVVWweTOsWlU6idRZUUfEFyJifUTcExHfj4jdmg4mFTU4CHPnuvpDrTBhUUfEHOBzwGBmHg5MB85qOphU1Oj4Y80aePnl0mnU5zodfQwAu0fEALAH8PvmIkktMTQEr70GV15ZOon63IRFnZmPA98EHgGeAF7IzKvGPi4izo2ItRGxdnh4ePKTSt12wgkwa5arP1RcJ6OPfYGlwHzgvcCeEXH22Mdl5gWZOZiZg7Nnz578pFK3DQzUV9RbtQpef710GvWxTkYfpwK/y8zhzHwDuAQ4rtlYUktUFbz4Ilx7bekk6mOdFPUjwDERsUdEBHAKsKHZWFJLnHIKzJzp6g8V1cmM+lbgx8CdwK9G/swFDeeS2mHXXev9FC+7rF5XLRXQ0aqPzPznzDw0Mw/PzE9mpgM79Y+qguFhuPHG0knUpzwzUZrIokX1kbXjDxViUUsT2WsvWLiwLmp3KFcBFrXUiaqCRx+FO+4onUR9yKKWOnHmmTB9uuMPFWFRS53Ybz846STPUlQRFrXUqaqC++6De+8tnUR9xqKWOrVsWX1VPY+q1WUWtdSp97wHjjnGObW6zqKWtkdVwZ13wkMPlU6iPmJRS9tjdIfyFSvK5lBfsail7XHQQXDEEY4/1FUWtbS9qgpuuAGeeqp0EvUJi1raXkND9ankl11WOon6hEUtba/DD4eDD3b8oa6xqKXtNbpD+bXXwvPPl06jPmBRSzuiquCNN2D16tJJ1AcsamlHHHUUzJnj+ENdYVFLO2LatPpNxZ/8BF55pXQaTXEWtbSjhobg1VdhzZrSSTTFWdTSjjrxxPryp44/1DCLWtpRAwOwdCmsWgUbN5ZOoynMopZ2RlXVS/R+9rPSSTSFWdTSzjj11HrzW8cfapBFLe2M3XaDxYvrq+lt3lw6jaYoi1raWVVVX6Dp5ptLJ9EUZVFLO2vxYpgxwy261BiLWtpZM2fCaafVc+rM0mk0BVnU0mSoqnp7rnXrSifRFGRRS5PhzDPr08pd/aEGWNTSZJg1Cz76UYtajbCopclSVXDvvXD//aWTaIqxqKXJsmxZ/dHVH5pkFrU0WebOhaOPdvyhSWdRS5OpquD22+GRR0on0RRiUUuTaWio/rhiRdkcmlI6KuqI2CcifhwR90XEhog4tulgUk865JB6l3LHH5pEnR5R/ztwZWYeChwBbGguktTjhobg+uvh6adLJ9EUMWFRR8TewInAhQCZuTEzn286mNSzqgq2bIGVK0sn0RTRyRH1+4Fh4H8j4q6I+E5E7Dn2QRFxbkSsjYi1w8PDkx5U6hlHHAHz57tMT5Omk6IeAD4C/E9mHgn8H/CVsQ/KzAsyczAzB2fPnj3JMaUeElEfVV99NbzwQuk0mgI6KerHgMcy89aRr39MXdyStmZoqN5HcfXq0kk0BUxY1Jn5JPBoRPzJyE2nAPc2mkrqdcceC/vv7/hDk2Kgw8d9Frg4ImYADwJ/3VwkaQqYNq0+pfy734VXX4Xddy+dSD2so+V5mbluZP78ocxclpl/aDqY1POqCl55Ba66qnQS9TjPTJSa8rGPwT77OP7QTrOopabssgssWVKvp37jjdJp1MMsaqlJVQV/+AP8/Oelk6iHWdRSk047DfbYw2t/aKdY1FKTdt8dPvGJek69ZUvpNOpRFrXUtKqCJ5+EW24pnUQ9yqKWmnb66fUbi67+0A6yqKWmvetdcOqp9Zw6s3Qa9SCLWuqGqoIHH4Rf/rJ0EvUgi1rqhiVL6tPKXf2hHWBRS93w7nfDCSdY1NohFrXULVUF99wDv/lN6STqMRa11C2jO5S7+kPbyaKWuuXAA2FwEJYvL51EPcailrqpquC22+Cxx0onUQ+xqKVuGh1/rFhRNod6ikUtddOhh8Jhh7n6Q9vFopa6rargF7+AZ54pnUQ9wqKWuq2qYPNmuPzy0knUIyxqqduOPBLe9z7HH+qYRS11W0T9puJVV8FLL5VOox5gUUslVBVs3AhXXFE6iXqARS2VcNxx9fU/PEtRHbCopRKmT4dly2D1anjttdJp1HIWtVTK0BC8/DJcfXXpJGo5i1oq5eSTYe+9Xf2hCVnUUikzZsCZZ8LKlbBpU+k0ajGLWiqpquDZZ+H660snUYtZ1FJJCxfCbrs5/tA2WdRSSXvuCYsW1cv0tmwpnUYtZVFLpVUVPP443H576SRqKYtaKu2MM2BgwJNftFUWtVTavvvWS/WWL4fM0mnUQha11AZVBQ88AOvXl06iFrKopTZYurS+qp6rPzSOjos6IqZHxF0RsarJQFJf2n//+kJNFrXGsT1H1OcBG5oKIvW9qoK774bf/rZ0ErVMR0UdEXOB04HvNBtH6mOjO5S7+kNjdHpE/W3gy8BWV+RHxLkRsTYi1g4PD09KOKmvzJ9fb9NlUWuMCYs6Is4Ans7MO7b1uMy8IDMHM3Nw9uzZkxZQ6itVBTfdBE88UTqJWqSTI+rjgSUR8RDwA+DkiPheo6mkfjU6/lixomwOtcqERZ2Z52fm3MycB5wFXJuZZzeeTOpHCxbAIYe4+kNv4zpqqU0i6vHHddfBc8+VTqOW2K6izsyfZeYZTYWRRF3UmzfD5ZeXTqKW8IhaapvBQZg719UfepNFLbXN6PhjzZp681v1PYtaaqOqgtdegyuvLJ1ELWBRS210wgkwa1Z96VP1PYtaaqPp0+sr6q1eDa+/XjqNCrOopbaqKnjpJbjmmtJJVJhFLbXVKafAzJmu/pBFLbXWrrvW+ymuWFGvq1bfsqilNqsqeOYZuOGG0klUkEUttdmiRfWRtdf+6GsWtdRme+0FCxfWc2p3KO9bFrXUdlUFjz4Kd2zzkvCawixqqe3OPLNeV+34o29Z1FLb7bcfnHRSfZai44++ZFFLvaCq4Ne/hg0bSidRARa11AuWLq0/Ov7oSxa11Ave+1449liLuk9Z1FKvqCq46y546KHSSdRlFrXUK0Z3KPfaH33HopZ6xUEHwRFHOP7oQxa11EuqCm68EZ58snQSdZFFLfWSoaF6LfVll5VOoi6yqKVecvjhcPDBzqn7jEUt9ZLRHcqvuQaef750GnWJRS31mqqCTZtg1arSSdQlFrXUa446CubMcfVHH7GopV4zbRosWwZXXgmvvFI6jbrAopZ6UVXBq6/CmjWlk6gLLGqpF514Yn35U8cffcGilnrRwEB9Rb3LL4eNG0unUcMsaqlXDQ3BCy/AddeVTqKGWdRSr/r4x2HPPR1/9AGLWupVu+0Gp59en06+eXPpNGqQRS31sqqCp56Cm28unUQNsqilXrZ4McyY4fhjipuwqCPigIi4LiI2RMT6iDivG8EkdWDmzHpWfckl7lA+hXVyRL0J+GJmHgYcA3wmIhY0G0tSx6oKHn4Y1q0rnUQNmbCoM/OJzLxz5POXgA3AnKaDSerQkiX1aeWOP6as7ZpRR8Q84Ejg1nHuOzci1kbE2uHh4clJJ2lis2bBgQfWR9Wakjou6ojYC1gOfD4zXxx7f2ZekJmDmTk4e/bsycwoaSIRpROoQR0VdUTsQl3SF2emr68kqYs6WfURwIXAhsz8VvORJElv1ckR9fHAJ4GTI2LdyH+LG84lSRoxMNEDMvMGwAGYJBXimYmS1HIWtSS1nEUtSS1nUUtSy1nUktRyFrUktZxFLUktZ1FLUstZ1JLUcha1JLWcRS1JLWdRS1LLWdSS1HIWtSS1nEUtSS1nUUtSy1nUktRyFrUktZxFLUktZ1FLUstZ1JLUcha1JLWcRS1JLWdRS1LLWdSS1HIWtSS1nEUtSS1nUUtSy1nUktRyFrUktZxFLUktZ1FLUstZ1JLUcha1JLWcRS1JLddRUUfEooi4PyIeiIivNB1KkvRHExZ1REwH/gv4BLAA+KuIWNB0MElSbaCDxxwNPJCZDwJExA+ApcC9TQZTIZ/7HFxzTekU2l6PPlo6gRrUSVHPAd76LHgM+LOxD4qIc4FzAQ488MBJCacCDjgAFviCqecsWADnnFM6hRrSSVHHOLflO27IvAC4AGBwcPAd96tHfOlLpRNIGqOTNxMfAw54y9dzgd83E0eSNFYnRX078IGImB8RM4CzgJXNxpIkjZpw9JGZmyLi74E1wHTgosxc33gySRLQ2YyazLwCuKLhLJKkcXhmoiS1nEUtSS1nUUtSy1nUktRykTn556ZExDDwMDALeGbSf0BzzNss8zbLvM1qOu/7MnP2eHc0UtRvfvOItZk52NgPmGTmbZZ5m2XeZpXM6+hDklrOopaklmu6qC9o+PtPNvM2y7zNMm+ziuVtdEYtSdp5jj4kqeUsaklquUaKOiL+IiLWR8SWiBgcc9/5I5vk3h8RC5v4+TsjIj4cEbdExLqIWBsRR5fONJGI+OzI/8/1EfH10nk6ERH/EBEZEbNKZ9mWiPhGRNwXEb+MiEsjYp/Smcbqpc2nI+KAiLguIjaMPF/PK52pExExPSLuiohVJX5+U0fU9wAV8Iu33jiyKe5ZwAeBRcB/j2ye2yZfB/4lMz8M/NPI160VESdR72H5ocz8IPDNwpEmFBEHAB8HHimdpQM/BQ7PzA8BvwbOL5znbXpw8+lNwBcz8zDgGOAzLc876jxgQ6kf3khRZ+aGzLx/nLuWAj/IzNcz83fAA9Sb57ZJAnuPfP4u2r+bzaeBf83M1wEy8+nCeTrxb8CXGWdLt7bJzKsyc9PIl7dQ73DUJm9uPp2ZG4HRzadbKTOfyMw7Rz5/ibr85pRNtW0RMRc4HfhOqQzdnlGPt1Fu2/6SPg98IyIepT46bdUR1DgOAf48Im6NiJ9HxFGlA21LRCwBHs/Mu0tn2QF/A/ykdIgxeuF3alwRMQ84Eri1bJIJfZv6wGJLqQAdbRwwnoi4Gth/nLu+lpmXbe2PjXNb14+qtpUdOAX4QmYuj4i/BC4ETu1mvrEmyDsA7Ev9MvIo4IcR8f4suO5ygrxfBU7rbqJt6+S5HBFfo37ZfnE3s3WgFb9T2ysi9gKWA5/PzBdL59maiDgDeDoz74iIj5XKscNFnZk7Ul6t2Ch3W9kj4rvU8yiAH1Hw5c6oCfJ+GrhkpJhvi4gt1BePGe5WvrG2ljci/hSYD9wdEVD//d8ZEUdn5pNdjPg2Ez2XI+Ic4AzglJL/AG5FK36ntkdE7EJd0hdn5iWl80zgeGBJRCwGdgP2jojvZebZ3QzR7dHHSuCsiNg1IuYDHwBu63KGifwe+OjI5ycDvymYpRMrqHMSEYcAM2jpFcky81eZ+e7MnJeZ86hL5iMlS3oiEbEI+EdgSWa+UjrPOHpq8+mo/4W+ENiQmd8qnWcimXl+Zs4deb6eBVzb7ZKGnTii3paIGAL+A5gNrI6IdZm5MDPXR8QPgXupX0Z+JjM3N5FhJ/wt8O8RMQC8BpxbOM9ELgIuioh7gI3AOS086utl/wnsCvx05FXALZn5d2Uj/VEPbj59PPBJ4FcRsW7ktq+O7MuqrfAUcklqOc9MlKSWs6glqeUsaklqOYtaklrOopaklrOoJanlLGpJarn/B1/ZqUoFFFbqAAAAAElFTkSuQmCC\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "# Segmente zusammenfassen zu shape\n", + "segment_list = []\n", + "segment_list.append(bottom)\n", + "segment_list.append(root_face)\n", + "segment_list.append(groove_face)\n", + "segment_list.append(top)\n", + "shape = geo.Shape2D(segment_list)\n", + "\n", + "shape_data = shape.rasterize(0.8)\n", + "\n", + "ax = plt.gca()\n", + "ax.cla()\n", + "ax.axis(\"equal\")\n", + "\n", + "plt.plot(shape_data[0],shape_data[1],\"r-\")" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "1.0\n" + ] + }, + { + "data": { + "text/plain": [ + "[]" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "# shape verschieben und verdoppeln\n", + "b_halbe = v_naht_dict[\"b\"].to_value(\"millimeter\")/2\n", + "print(b_halbe)\n", + "shape.translate([-b_halbe, 0])\n", + "shape_r = copy.deepcopy(shape)\n", + "shape_r.reflect([-b_halbe, 0])\n", + "\n", + "shape_r_data = shape_r.rasterize(0.8)\n", + "\n", + "ax = plt.gca()\n", + "ax.cla()\n", + "ax.axis(\"equal\")\n", + "\n", + "plt.plot(shape_r_data[0],shape_r_data[1],\"r-\")" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "# profil erstellen\n", + "profile = pcg.Profile([shape, shape_r])" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[]" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "profile_data = profile.rasterize(0.8)\n", + "shape_data = shape.rasterize(0.8)\n", + "shape_r_data = shape_r.rasterize(0.8)\n", + "\n", + "ax = plt.gca()\n", + "ax.cla()\n", + "ax.axis(\"equal\")\n", + "\n", + "plt.plot(shape_data[0],shape_data[1],\"r-\")\n", + "plt.plot(shape_r_data[0],shape_r_data[1],\"r-\")\n", + "#plt.plot(profile_data[0],profile_data[1],\"--\")" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[]" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "h = len(profile_data[0])//2\n", + "\n", + "ax = plt.gca()\n", + "ax.cla()\n", + "ax.axis(\"equal\")\n", + "\n", + "plt.plot(profile_data[0][:h],\n", + " profile_data[1][:h], \"-\")\n", + "plt.plot(profile_data[0][h:],\n", + " profile_data[1][h:], \"-\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Loading from file" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "import matplotlib.pyplot as plt\n", + "from mypackage.all_groove import singleVGrooveButtWeld, singleUGrooveButtWeld\n", + "from astropy.units import Quantity" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "# Values for the Single V Groove Butt Weld\n", + "# in a dictionary\n", + "t = Quantity(0.009,unit=\"meter\")\n", + "alpha = Quantity(40, unit=\"deg\")\n", + "b = Quantity(0, unit=\"centimeter\")\n", + "c = Quantity(1, unit=\"millimeter\")\n", + "v_naht_dict = dict(t=t, alpha=alpha, b=b, c=c)" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [], + "source": [ + "profile = singleVGrooveButtWeld(v_naht_dict)" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[]" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "profile_data = profile.rasterize(0.2)\n", + "\n", + "ax = plt.gca()\n", + "ax.cla()\n", + "#ax.axis(\"equal\")\n", + "ax.axis([-7, +7, -1, 20])\n", + "ax.grid(True)\n", + "ax.set(title=\"single V Groove Butt Weld {}° groove angle\".format(alpha.value), xlabel=\"millimeter\", ylabel=\"millimeter\")\n", + "\n", + "plt.plot(profile_data[0],profile_data[1],\"-\")" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [], + "source": [ + "# Values for the Single V Groove Butt Weld\n", + "# in a dictionary\n", + "t = Quantity(0.009,unit=\"meter\")\n", + "alpha = Quantity(60, unit=\"deg\")\n", + "b = Quantity(0.2, unit=\"centimeter\")\n", + "c = Quantity(1, unit=\"millimeter\")\n", + "v_naht_dict = dict(t=t, alpha=alpha, b=b, c=c)" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [], + "source": [ + "profile = singleVGrooveButtWeld(v_naht_dict)" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[]" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "profile_data = profile.rasterize(0.2)\n", + "\n", + "ax = plt.gca()\n", + "ax.cla()\n", + "#ax.axis(\"equal\")\n", + "ax.axis([-7, +7, -1, 20])\n", + "#[xmin, xmax, ymin, ymax]\n", + "ax.grid(True)\n", + "ax.set(title=\"single V Groove Butt Weld {}° groove angle\".format(alpha.value), xlabel=\"millimeter\", ylabel=\"millimeter\")\n", + "\n", + "plt.plot(profile_data[0],profile_data[1],\"x\")" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [], + "source": [ + "t = Quantity(15, unit=\"millimeter\")\n", + "beta = Quantity(9, unit=\"deg\")\n", + "R = Quantity(6, unit=\"millimeter\")\n", + "b = Quantity(3, unit=\"millimeter\")\n", + "c = Quantity(1, unit=\"millimeter\")\n", + "u_naht_dict = dict(t=t, beta=beta, R=R, b=b, c=c)" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [], + "source": [ + "profile = singleUGrooveButtWeld(u_naht_dict)" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[]" + ] + }, + "execution_count": 23, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "profile_data = profile.rasterize(0.2)\n", + "\n", + "ax = plt.gca()\n", + "ax.cla()\n", + "ax.axis(\"equal\")\n", + "#ax.axis([-7, +7, -1, 20])\n", + "#[xmin, xmax, ymin, ymax]\n", + "ax.grid(True)\n", + "ax.set(title=\"single U Groove Butt Weld {}° groove angle\".format(beta.value), xlabel=\"millimeter\", ylabel=\"millimeter\")\n", + "\n", + "plt.plot(profile_data[0],profile_data[1],\"x\")" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [ + { + "ename": "TypeError", + "evalue": "__init__() missing 1 required positional argument: 'points'", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", + "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mgeo\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mArcSegment\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", + "\u001b[0;31mTypeError\u001b[0m: __init__() missing 1 required positional argument: 'points'" + ] + } + ], + "source": [ + "geo.ArcSegment()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "libo (stable)", + "language": "python", + "name": "libo_stable" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.6" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/mypackage/all_groove.py b/mypackage/all_groove.py new file mode 100644 index 0000000..cd6ab83 --- /dev/null +++ b/mypackage/all_groove.py @@ -0,0 +1,120 @@ +"""provides the calculation of all Groove-Types""" + +from astropy.units import Quantity +import numpy as np +import copy + +import mypackage.geometry as geo +import mypackage.point_cloud_generator as pcg + + +def singleVGrooveButtWeld(d, width_default=Quantity(5, unit="millimeter")): + """ + The calculation of a Single-V Groove Butt Weld. + Required variables in the dictionary are in Quantity(astropy): + t: the workpiece thickness + alpha: the groove angle + b: the root opening + c: the root face + + + :param d: input dictionary with astropy Quantity + :param width_default: the width of the workpiece + :return: point_could_generator.Profile + """ + t = d["t"].to_value("millimeter") + alpha = d["alpha"].to_value("rad") + b = d["b"].to_value("millimeter") + c = d["c"].to_value("millimeter") + width = width_default.to_value("millimeter") + + segment_list = [] + + bottom = geo.LineSegment([[-width, 0], [0, 0]]) + segment_list.append(bottom) + + if c != 0: + root_face = geo.LineSegment([[0, 0], [0, c]]) + segment_list.append(root_face) + + s = np.tan(alpha / 2) * (t - c) + groove_face = geo.LineSegment([[0, -s], [c, t]]) + segment_list.append(groove_face) + + top = geo.LineSegment([[-s, -width], [t, t]]) + segment_list.append(top) + + shape = geo.Shape2D(segment_list) + + shape.translate([-b / 2, 0]) + shape_r = copy.deepcopy(shape) + if b != 0: + shape_r.reflect([-b / 2, 0]) + else: + shape_r.reflect([-1, 0]) + + profile = pcg.Profile([shape, shape_r]) + + return profile + + +def singleUGrooveButtWeld(d, width_default=Quantity(15, unit="millimeter")): + """ + The calculation of a Single-U Groove Butt Weld. + Required variables in the dictionary are in Quantity(astropy): + t: the workpiece thickness + beta: the bevel angle + R: radius + b: the root opening + c: the root face + + + :param d: input dictionary with astropy Quantity + :param width_default: the width of the workpiece + :return: point_could_generator.Profile + """ + + t = d["t"].to_value("millimeter") + beta = d["beta"].to_value("rad") + R = d["R"].to_value("millimeter") + b = d["b"].to_value("millimeter") + c = d["c"].to_value("millimeter") + width = width_default.to_value("millimeter") + + segment_list = [] + + bottom = geo.LineSegment([[-width, 0], [0, 0]]) + segment_list.append(bottom) + + if c != 0: + root_face = geo.LineSegment([[0, 0], [0, c]]) + segment_list.append(root_face) + + # vom nächsten Punkt zum Kreismittelpunkt ist der Vektor (x,y) + x = R * np.cos(beta) + y = R * np.sin(beta) + # m = [0,c+R] Kreismittelpunkt + # => [-x,c+R-y] ist der nächste Punkt + groove_face_arc = geo.ArcSegment([[0, -x, 0], + [c, c + R - y, c + R]], False) + segment_list.append(groove_face_arc) + + s = np.tan(beta) * (t - (c + R - y)) + groove_face_line = geo.LineSegment([[-x, -x - s], [c + R - y, t]]) + segment_list.append(groove_face_line) + + top = geo.LineSegment([[-x - s, -width], [t, t]]) + segment_list.append(top) + + shape = geo.Shape2D(segment_list) + + shape.translate([-b / 2, 0]) + shape_r = copy.deepcopy(shape) + if b != 0: + shape_r.reflect([-b / 2, 0]) + else: + shape_r.reflect([-1, 0]) + + profile = pcg.Profile([shape, shape_r]) + + return profile diff --git a/mypackage/mark_funcs.py b/mypackage/mark_funcs.py deleted file mode 100644 index 8ea440f..0000000 --- a/mypackage/mark_funcs.py +++ /dev/null @@ -1,37 +0,0 @@ -"""Test Functions by Markus.""" -import xarray as xr -import numpy as np - - -def function_one(): - """ - Print "Hello World". - - :param: --- - :return: --- - """ - print("Hello World") - - -def function_two(): - """ - Working with Xarrays. - - :param: --- - :return: --- - """ - xcoords = np.linspace(0, 1, 2) - ycoords = np.linspace(0, 1, 2) - my_array = xr.DataArray( - [[True, False], [False, True]], - name=["xcoords", "ycoords"], - dims=["x", "y"], - coords=dict(x=xcoords, y=ycoords), - ) - try: - if my_array.sel(x=1, y=1, method="nearest", tolerance=0): - raise KeyError - else: - my_array.loc[dict(x=1, y=1)] = True - except KeyError: - print("Punkt schon vorhanden!") diff --git a/tests/test_mark_funcs.py b/tests/test_mark_funcs.py deleted file mode 100644 index 9d8a4ab..0000000 --- a/tests/test_mark_funcs.py +++ /dev/null @@ -1,9 +0,0 @@ -import mypackage.mark_funcs as mf - - -def test_function_one(): - mf.function_one() - - -def test_function_two(): - mf.function_two() From 7620596eddda2146bc574d0001e5b7959948bf9c Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Tue, 11 Feb 2020 10:43:24 +0100 Subject: [PATCH 165/177] Test visaulization package --- codecov.yml | 1 - tests/test_visualization.py | 21 +++++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 tests/test_visualization.py diff --git a/codecov.yml b/codecov.yml index cbbb0e3..c371f9b 100644 --- a/codecov.yml +++ b/codecov.yml @@ -25,7 +25,6 @@ coverage: ignore: - "tests" - "*__init__.py" - - "*visualization.py" #comment: #layout: "reach, diff, flags, files" diff --git a/tests/test_visualization.py b/tests/test_visualization.py new file mode 100644 index 0000000..3df9f5c --- /dev/null +++ b/tests/test_visualization.py @@ -0,0 +1,21 @@ +"""Test functions of the visualization package.""" + +import mypackage.visualization as vs +import mypackage.transformations as tf + +from mpl_toolkits.mplot3d import Axes3D # noqa: F401 unused import +import matplotlib.pyplot as plt + + +def test_plot_coordinate_system(): + cs = tf.CoordinateSystem() + fig = plt.figure() + ax = fig.gca(projection='3d') + + vs.plot_coordinate_system(cs, ax) + + +def test_set_axes_equal(): + fig = plt.figure() + ax = fig.gca(projection='3d') + vs.set_axes_equal(ax) From 4e99590a49ad5080c50f96b8f78b60e24a37b35f Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Tue, 11 Feb 2020 12:00:44 +0100 Subject: [PATCH 166/177] Add color and label to plot_coordinate_system --- mypackage/visualization.py | 29 +++++++++++++++++++++++++---- tests/test_visualization.py | 13 ++++++++++++- 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/mypackage/visualization.py b/mypackage/visualization.py index 78f41e7..3a9cb35 100644 --- a/mypackage/visualization.py +++ b/mypackage/visualization.py @@ -3,12 +3,28 @@ import numpy as np -def plot_coordinate_system(coordinate_system, axes): +def _check_color_key_valid(color_key): + """ + Check if a color key is valid. + + :param color_key: Matplotlib color key letter (e.g. 'r' for red etc). + :return: --- + """ + valid_colors = ["r", "g", "b", "y", "c", "m", "k", "w"] + if color_key not in valid_colors: + raise Exception("Invalid color key.") + + +def plot_coordinate_system(coordinate_system, axes, color=None, label=None): """ Plot a coordinate system in a matplotlib 3d plot. :param coordinate_system: Coordinate system :param axes: Matplotlib axes object (output from plt.gca()) + :param color: Matplotlib color key letter (e.g. 'r' for red etc). The + origin of the coordinate system will be marked with this color. + :param label: Name that appears in the legend. Only viable if a color + was specified. :return: --- """ p0 = coordinate_system.origin @@ -16,9 +32,14 @@ def plot_coordinate_system(coordinate_system, axes): py = p0 + coordinate_system.orientation[:, 1] pz = p0 + coordinate_system.orientation[:, 2] - axes.plot([p0[0], px[0]], [p0[1], px[1]], [p0[2], px[2]], 'r') - axes.plot([p0[0], py[0]], [p0[1], py[1]], [p0[2], py[2]], 'g') - axes.plot([p0[0], pz[0]], [p0[1], pz[1]], [p0[2], pz[2]], 'b') + axes.plot([p0[0], px[0]], [p0[1], px[1]], [p0[2], px[2]], "r") + axes.plot([p0[0], py[0]], [p0[1], py[1]], [p0[2], py[2]], "g") + axes.plot([p0[0], pz[0]], [p0[1], pz[1]], [p0[2], pz[2]], "b") + if color is not None: + _check_color_key_valid(color) + axes.plot([p0[0]], [p0[1]], [p0[2]], color + "o", label=label) + elif label is not None: + raise Exception("Labels can only be assigned if a color was specified") def set_axes_equal(axes): diff --git a/tests/test_visualization.py b/tests/test_visualization.py index 3df9f5c..c9b5ab9 100644 --- a/tests/test_visualization.py +++ b/tests/test_visualization.py @@ -5,6 +5,7 @@ from mpl_toolkits.mplot3d import Axes3D # noqa: F401 unused import import matplotlib.pyplot as plt +import pytest def test_plot_coordinate_system(): @@ -12,7 +13,17 @@ def test_plot_coordinate_system(): fig = plt.figure() ax = fig.gca(projection='3d') - vs.plot_coordinate_system(cs, ax) + vs.plot_coordinate_system(cs, ax, "g") + vs.plot_coordinate_system(cs, ax, "r", "test") + + # exceptions ------------------------------------------ + + # invalid color + with pytest.raises(Exception): + vs.plot_coordinate_system(cs, ax, color="color") + # label without color + with pytest.raises(Exception): + vs.plot_coordinate_system(cs, ax, label="label") def test_set_axes_equal(): From 86bcdb241f3c872004ad78106fd612d2eacfd13e Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Tue, 11 Feb 2020 12:06:24 +0100 Subject: [PATCH 167/177] Remove tests from codacy duplication checks --- .codacy.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.codacy.yml b/.codacy.yml index 37268f6..153f946 100644 --- a/.codacy.yml +++ b/.codacy.yml @@ -4,6 +4,11 @@ engines: exclude_paths: - tests/** +duplication: + enabled: true + exclude_paths: + - tests/** + exclude_paths: - 'docs/**' - 'scripts/**' From 3a5edf90c44571ff1725faefff24038637c759d7 Mon Sep 17 00:00:00 2001 From: Hirthammer Date: Tue, 11 Feb 2020 12:13:31 +0100 Subject: [PATCH 168/177] Try to fix codacy issue --- tests/test_visualization.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_visualization.py b/tests/test_visualization.py index c9b5ab9..1663ab6 100644 --- a/tests/test_visualization.py +++ b/tests/test_visualization.py @@ -3,7 +3,10 @@ import mypackage.visualization as vs import mypackage.transformations as tf +# pylint: disable=W0611 from mpl_toolkits.mplot3d import Axes3D # noqa: F401 unused import +# pylint: enable=W0611 + import matplotlib.pyplot as plt import pytest From 78781b90dd9c3946701922ae0e43adc453fb3bef Mon Sep 17 00:00:00 2001 From: mnagel Date: Wed, 12 Feb 2020 10:58:05 +0100 Subject: [PATCH 169/177] Update naming for the changes with the geometry package change --- Weld_tester.ipynb | 133 +++++++++++++++++----------------------- mypackage/all_groove.py | 32 ++++------ 2 files changed, 68 insertions(+), 97 deletions(-) diff --git a/Weld_tester.ipynb b/Weld_tester.ipynb index fd14836..54bf971 100644 --- a/Weld_tester.ipynb +++ b/Weld_tester.ipynb @@ -12,7 +12,6 @@ "import copy\n", "\n", "import mypackage.geometry as geo\n", - "import mypackage.point_cloud_generator as pcg\n", "import mypackage.transformations as tr" ] }, @@ -25,18 +24,20 @@ "name": "stdout", "output_type": "stream", "text": [ - "[[2. 1.86666667 1.73333333 1.6 1.46666667 1.33333333\n", - " 1.2 1.06666667 0.93333333 0.8 0.66666667 0.53333333\n", - " 0.4 0.26666667 0.13333333 0. ]\n", - " [0. 0.06666667 0.13333333 0.2 0.26666667 0.33333333\n", - " 0.4 0.46666667 0.53333333 0.6 0.66666667 0.73333333\n", - " 0.8 0.86666667 0.93333333 1. ]]\n" + "[[2. 1.89473684 1.78947368 1.68421053 1.57894737 1.47368421\n", + " 1.36842105 1.26315789 1.15789474 1.05263158 0.94736842 0.84210526\n", + " 0.73684211 0.63157895 0.52631579 0.42105263 0.31578947 0.21052632\n", + " 0.10526316 0. ]\n", + " [3. 2.89473684 2.78947368 2.68421053 2.57894737 2.47368421\n", + " 2.36842105 2.26315789 2.15789474 2.05263158 1.94736842 1.84210526\n", + " 1.73684211 1.63157895 1.52631579 1.42105263 1.31578947 1.21052632\n", + " 1.10526316 1. ]]\n" ] }, { "data": { "text/plain": [ - "[]" + "[]" ] }, "execution_count": 2, @@ -45,7 +46,7 @@ }, { "data": { - "image/png": "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\n", + "image/png": "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\n", "text/plain": [ "
" ] @@ -57,7 +58,7 @@ } ], "source": [ - "seg = geo.LineSegment([[2,0],[0,1]])\n", + "seg = geo.LineSegment([[2,0],[3,1]])\n", "seg_data = seg.rasterize(0.15)\n", "print(seg_data)\n", "plt.plot(seg_data[0], seg_data[1])" @@ -120,7 +121,7 @@ { "data": { "text/plain": [ - "[]" + "[]" ] }, "execution_count": 5, @@ -147,7 +148,7 @@ "segment_list.append(root_face)\n", "segment_list.append(groove_face)\n", "segment_list.append(top)\n", - "shape = geo.Shape2D(segment_list)\n", + "shape = geo.Shape(segment_list)\n", "\n", "shape_data = shape.rasterize(0.8)\n", "\n", @@ -173,7 +174,7 @@ { "data": { "text/plain": [ - "[]" + "[]" ] }, "execution_count": 6, @@ -182,7 +183,7 @@ }, { "data": { - "image/png": "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\n", + "image/png": "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\n", "text/plain": [ "
" ] @@ -197,11 +198,10 @@ "# shape verschieben und verdoppeln\n", "b_halbe = v_naht_dict[\"b\"].to_value(\"millimeter\")/2\n", "print(b_halbe)\n", - "shape.translate([-b_halbe, 0])\n", - "shape_r = copy.deepcopy(shape)\n", - "shape_r.reflect([-b_halbe, 0])\n", + "shape = shape.translate([-b_halbe, 0])\n", + "shape_r = shape.reflect_across_line([0, 0], [0, 1])\n", "\n", - "shape_r_data = shape_r.rasterize(0.8)\n", + "shape_r_data = shape_r.rasterize(0.1)\n", "\n", "ax = plt.gca()\n", "ax.cla()\n", @@ -217,27 +217,27 @@ "outputs": [], "source": [ "# profil erstellen\n", - "profile = pcg.Profile([shape, shape_r])" + "profile = geo.Profile([shape, shape_r])" ] }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 11, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "[]" + "[]" ] }, - "execution_count": 8, + "execution_count": 11, "metadata": {}, "output_type": "execute_result" }, { "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAWoAAAD4CAYAAADFAawfAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4xLjEsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy8QZhcZAAAX9ElEQVR4nO3de4xW9Z3H8feXGeRmqVVGyqIwpdUiVKR0vFC8IIjcRmBOu0k3adPsZmPadF272d1uL2n2v2a3u9ltk91sYtpusqnZZqNnYLiDKFraog5SpTrVYhFBUAdvKF64ffeP30wtyMw8M/Oc53fOeT6vhHCZR/IRZj6c53t+5/czd0dERPJrROwAIiLSPxW1iEjOqahFRHJORS0iknMqahGRnGvM4jedMGGCNzc3Z/Fbi4iU0q5du464e9O5PpZJUTc3N9PZ2ZnFby0iUkpmtr+vj2n0ISKScypqEZGcU1GLiOScilpEJOdU1CIiOaeiFhHJORW1iEjOZbKOOjc2boRf/Sp2itr7/Odh1qzYKaRIHn0U1q6NnaL2broJFi6MnWJA5S7qrVvhBz+InaK23OHnP4cHHoidRIrkq1+FXbvALHaS2jIrRFFbFgcHtLS0uJ5MjOS734XvfQ9eegkmTIidRopg/35obobvfx/+/u9jp6lbZrbL3VvO9THNqMsmSeD0aejoiJ1EiqK9PXzf1hY3h/RJRV02s2eHq6M0jZ1EiiJNwz2NT3widhLpg4q6bMzCldHWrXD0aOw0kncvvQQ7duhqOudU1GWUJHD8eFj1ItKfjo5wAzpJYieRfqioy2juXJg4UeMPGViawsc/DldeGTuJ9ENFXUYNDbBqFaxfD+++GzuN5NUbb8C2beFqut6W5RWMirqskgSOHQuzapFzWb8eTpzQ2KMAVNRlNX8+fPjDGn9I39IUJk2Ca66JnUQGoKIuq/POg9tuCzeLTp6MnUby5u23w83mtjYYoRrIO/0NlVmSwKuvwkMPxU4iebNlSyhrjT0KQUVdZosXw5gxGn/IB6UpXHgh3Hhj7CRSARV1mY0dC0uXhkeET5+OnUby4sSJsFPeihUwcmTsNFIBFXXZtbXBoUPwyCOxk0hebN8Or7+upxELREVddq2t0Nio8Ye8L01h3DhYtCh2EqmQirrsLrgg7LebpuFRYalvp06FUdiyZeH+hRSCiroeJAk8+yzs2RM7icS2c2fYiEljj0JRUdeDlSvDI8K9+w5L/WpvD2vsly+PnUQGoaKiNrO/MbMnzew3Zva/ZjY662BSRRMnwvXXa05d79zD58Att8D48bHTyCAMWNRmNhn4a6DF3T8FNABfyDqYVFlbGzzxBOzdGzuJxPL447Bvnx5yKaBKRx+NwBgzawTGAoeyiySZ6J1JavxRv9I0PC6+YkXsJDJIAxa1u78A/CvwPHAYeMPdt5z9OjO73cw6zayzu7u7+klleJqbYc4cjT/qWZqGJxGbmmInkUGqZPTxEWAl8DHgT4BxZvbFs1/n7ne5e4u7tzTpEyGfkiTc9T+kN0R15+mn4cknNfYoqEpGH7cA+9y9291PACnw2WxjSSZ6v0hXr46bQ2qvd+S1alXcHDIklRT188B1ZjbWzAxYCHRlG0syccUVMH26xh/1qL0drr4aLr00dhIZgkpm1A8D9wCPAXt6/pu7Ms4lWWlrC3s9vPJK7CRSKwcOhL1eNPYorIpWfbj7P7r7dHf/lLt/yd3fyzqYZCRJwmPEa9fGTiK10jvqUlEXlp5MrDef+Ux4+6tlevUjTWHGDLj88thJZIhU1PXGLFxZbd4Mb70VO41krbs7nPCjq+lCU1HXoySB994LZ+ZJuXV0hEMjVNSFpqKuR/PmhYcetPqj/NI0POw0e3bsJDIMKup61NAQdtRbvz5cWUs5HT0K990XrqbNYqeRYVBR16skgTffhG3bYieRrGzYAMePa+/pElBR16sFC8JWlxp/lFeahi1u586NnUSGSUVdr0aNCucprlkDJ0/GTiPV9s474Yp65cow6pJCU1HXsySBI0dgx47YSaTatm6FY8fgc5+LnUSqQEVdz5YsgdGjNf4oozQNBxvPnx87iVSBirqejRsHixeHpxR1Qnl5nDgRtghobQ3nI0rhqajrXZLAwYPQ2Rk7iVTLQw/Bq6/qIZcSUVHXu9ZWaGzU+KNM0hTGjAnvlqQUVNT17sIL4eab4d57Nf4og9Onwyhr6VIYOzZ2GqkSFbWEByJ+9zt46qnYSWS4Hn4YDh/W2KNkVNQSjmcy09anZdDeDiNHwvLlsZNIFamoBSZNCk+vaU5dbO7h73DhwrA0T0pDRS1BksDu3bBvX+wkMlR79sCzz2pvjxJSUUvQ+8Wt8UdxpWkYYa1cGTuJVJmKWoJp0+CqqzT+KLI0heuvDxsxSamoqOV9SQK//CW8+GLsJDJYe/eG0YdWe5SSilrelyThhtSaNbGTyGD1jqw0ny4lFbW8b+ZMuOwyjT+KKE3DCfNTp8ZOIhlQUcv7ek8ov/9+eO212GmkUi+8ADt36mq6xFTUcqa2tnCQwLp1sZNIpVavDt9rPl1aKmo509VXw+TJWqZXJO3tMH06XHFF7CSSERW1nGnEiHBVvWlTOCFE8u2VV2D7dl1Nl5yKWj4oScKZe5s3x04iA1m7Fk6dUlGXnIpaPuiGG+Cii7T6owjSFKZMgTlzYieRDKmo5YMaG2HFinC1dvx47DTSl7fegi1bwqjKLHYayZCKWs4tSeDo0bBUT/Jp40Z47z2NPeqAilrO7ZZb4Pzztfojz9IUmppg3rzYSSRjKmo5t9Gjw+bzq1eHm1WSL+++G9a6r1oFDQ2x00jGVNTStySBl18OGzVJvmzbFmbUGnvUBRW19G3pUhg1Sqs/8qi9HcaPhwULYieRGqioqM3sAjO7x8x+a2ZdZjY362CSAx/6ECxaFIpaJ5Tnx8mTYYfD1lY477zYaaQGKr2i/iGwyd2nA1cBXdlFklxJEnj+eXjssdhJpNeOHXDkiMYedWTAojaz8cCNwI8B3P24u7+edTDJidtuCzerNP7IjzQNN3uXLImdRGqkkivqaUA38N9mttvMfmRm485+kZndbmadZtbZ3d1d9aASyYQJcNNNWqaXF+7h72LJEhj3gS9DKalKiroRmAP8l7t/GjgGfPPsF7n7Xe7e4u4tTU1NVY4pUSUJdHWFbxJXZyccPKixR52ppKgPAgfd/eGen99DKG6pF6tWhe81/ogvTcMj/q2tsZNIDQ1Y1O7+InDAzD7Z80sLgacyTSX5MnkyXHutxh+xucO998LNN8NHPhI7jdRQpas+7gDuNrMngNnA97KLJLmUJLBrF+zfHztJ/XrqKfjd7zT2qEMVFbW7/7pn/jzL3Ve5uw7Uqze95/H1HvsktZemYZe8lStjJ5Ea05OJUpnLLoMrr9ScOqb2dvjsZ2HSpNhJpMZU1FK5tjb4+c/hpZdiJ6k/+/bB7t06abxOqailckkSbmh1dMROUn96b+SqqOuSiloqN2sWTJum8UcMaQqzZ4c/f6k7KmqpnFm4qt62Dd54I3aa+nH4cNhqVqs96paKWgYnSeDECVi/PnaS+rFmTRg5qajrlopaBufaa8OqA40/aqe9Pay6mTEjdhKJREUtgzNiRHikfONGeOed2GnK77XXwgHDSaKTxuuYiloGL0ng7bdhy5bYScpv3bpwUIDGHnVNRS2Dd9NNYa8JjT+yl6ZwySXQ0hI7iUSkopbBGzkyHCjQ0RFuLEo2jh2DTZvCqGmEvlTrmf72ZWiSBF5/HbZvj52kvDZtgnff1dhDVNQyRLfeCmPHavyRpfZ2uOgiuOGG2EkkMhW1DM2YMbBsWdhN7/Tp2GnK5/hxWLsWVqwIBwVIXVNRy9AlCbz4IuzcGTtJ+dx/Pxw9Cp/7XOwkkgMqahm6ZcvCjUWNP6ovTeH882HhwthJJAdU1DJ0H/4w3HJLKBX32GnK49SpMFJavhxGj46dRnJARS3DkyRhr+THH4+dpDx+8Qvo7tZqD/kDFbUMz4oVYY2vxh/Vk6YwahQsXRo7ieSEilqG5+KLw/IxnVBeHe7hz/LWW+FDH4qdRnJCRS3DlyTwm9/AM8/ETlJ8jz0Gzz+vsYecQUUtw7dqVfheV9XDl6bQ0BAe0RfpoaKW4ZsyJWwapDn18KVp2PTqootiJ5EcUVFLdSQJPPIIHDgQO0lxdXXBb3+rsYd8gIpaqqO3XFavjpujyHrfkfSOkkR6qKilOj75yXBUlObUQ9feDtddB5Mnx04iOaOilupJEnjwQThyJHaS4tm/H3bt0thDzklFLdWTJGEnvY6O2EmKp/edSFtb3BySSypqqZ7Zs2HqVK3+GIo0hSuvhE98InYSySEVtVSPWbiq3ro1bNEplXnpJdixQ2MP6ZOKWqorScKm9xs2xE5SHGvWhEfHNfaQPqiopbrmzoWJE7X6YzDa2+HjH4dZs2InkZxSUUt1NTSEdcDr14eDWaV/r78O27aFdyJmsdNITqmopfqSBI4dC7Nq6d/69XDihObT0i8VtVTf/Pnh9Bet/hhYmsKkSXDNNbGTSI5VXNRm1mBmu81sXZaBpATOOy/s/tbRASdPxk6TX2+/DRs3hpuII3TNJH0bzGfHnUBXVkGkZJIEXn0VHnoodpL82rIF3nlHYw8ZUEVFbWaXAMuBH2UbR0pj8WIYM0bjj/6kKVx4Idx4Y+wkknOVXlH/APgGcLqvF5jZ7WbWaWad3d3dVQknBTZ2bDjzr709PFYuZzp+HNauDWdOjhwZO43k3IBFbWatwMvuvqu/17n7Xe7e4u4tTU1NVQsoBZYkcOhQ2KdazrR9e1iap7GHVKCSK+p5wAozew74GbDAzH6aaSoph+XLobFRD7+cS3s7jBsHixbFTiIFMGBRu/u33P0Sd28GvgDc7+5fzDyZFN8FF8DChWEW6x47TX6cOhWKetkyGD06dhopAK0JkmwlCezdG04pl2DnzrARk8YeUqFBFbW7b3f31qzCSAmtXBkejdbqj/elaVhrvmxZ7CRSELqilmxNnAjXX6+i7uUe/iwWLYLx42OnkYJQUUv2kgSeeAKefTZ2kvgefxyee05jDxkUFbVkr3efZa3+CFfTI0aE9dMiFVJRS/amToU5c+Dee2MniS9Nw5OIEybETiIFoqKW2kiSsNrh0KHYSeJ5+ml48kmd5CKDpqKW2uidya5eHTdHTDppXIZIRS21ccUVMH16fc+p29vDvtOXXho7iRSMilpqp60NHnggbH9abw4cCHue6GpahkBFLbWTJOHx6bVrYyepvd6Rj5blyRCoqKV2PvOZ8La/Hh9+SVOYORMuvzx2EikgFbXUjlm4oty8Gd56K3aa2unuDifd6GpahkhFLbWVJPDee7BpU+wktbN2bTg8QUUtQ6SiltqaNw+amupr/JGm0NwMV10VO4kUlIpaaquhIeyot25duLIuu6NHYevWcDVtFjuNFJSKWmovSeDNN2HbtthJsrdhQzgfUWMPGQYVtdTeggVhi896GH+kKXz0ozB3buwkUmAqaqm9UaOgtRXWrIGTJ2Onyc4774Qr6lWrwo55IkOkzx6Jo60NjhyBX/widpLs3HcfHDumpxFl2FTUEseSJeFg1zKPP9I0HPA7f37sJFJwKmqJ4/zzYfHi8p5QfuIEdHTAbbeF8xFFhkFFLfEkCRw8CI8+GjtJ9T30UNh8Sqs9pApU1BJPa2tYV13GrU/TFMaMgVtvjZ1ESkBFLfFceCHcfHP5xh+nT4d/fJYuhbFjY6eRElBRS1xJAs88A11dsZNUzyOPwOHDGntI1aioJa5Vq8Kj1WVa/ZGmMHIkLF8eO4mUhIpa4po0KTy1V5aidg//LwsXhqV5IlWgopb4kgR274Z9+2InGb49e+DZZzX2kKpSUUt8vU/ulWH1R5qGUc6KFbGTSImoqCW+adPCXs1lGH+kKVx/PUycGDuJlIiKWvIhSeCXv4QXX4ydZOj27g2jD409pMpU1JIPSRJuxK1ZEzvJ0PWObrQJk1SZilryYeZMuOyyYo8/0jSctD51auwkUjIqasmH3hPK778fXnstdprBe+EF2LlTYw/JhIpa8iNJwkEC69bFTjJ4q1eH71XUkgEVteRHSwtMnlzM8UeawvTp4ZtIlQ1Y1GZ2qZk9YGZdZvakmd1Zi2BSh0aMCDfiNm8OJ6MUxSuvwIMP6mpaMlPJFfVJ4G/d/QrgOuBrZjYj21hSt5IknDW4eXPsJJVbuxZOnVJRS2YGLGp3P+zuj/X8+E2gC5icdTCpUzfcABddVKzxR3s7TJkCc+bETiIlNagZtZk1A58GHj7Hx243s04z6+zu7q5OOqk/jY0waxbs3x87SeX274fZs8PKFZEMVFzUZnY+cC/wdXc/evbH3f0ud29x95ampqZqZpR6U8TCK2JmKYyKitrMRhJK+m53L9B7UhGR4qtk1YcBPwa63P3fso8kIiJ/rJIr6nnAl4AFZvbrnm/LMs4lIiI9Ggd6gbvvADSAExGJRE8miojknIpaRCTnVNQiIjmnohYRyTkVtYhIzqmoRURyTkUtIpJzKmoRkZxTUYuI5JyKWkQk51TUIiI5p6IWEck5FbWISM6pqEVEck5FLSKScypqEZGcU1GLiOScilpEJOdU1CIiOaeiFhHJORW1iEjOqahFRHJORS0iknMqahGRnFNRi4jknIpaRCTnVNQiIjmnohYRyTkVtYhIzqmoRURyTkUtIpJzKmoRkZxTUYuI5JyKWkQk51TUIiI5V1FRm9kSM3vazPaa2TezDiUiIu8bsKjNrAH4T2ApMAP4MzObkXUwEREJGit4zTXAXnf/PYCZ/QxYCTyVZbCq+Od/hv/5n9gpZLCeew7mzImdYnC2bYOZM2OnkMH6ylfgjjtipxhQJUU9GTjwRz8/CFx79ovM7HbgdoApU6ZUJdywTZwIM3TxXzgzZkCSxE5RuTvvhA0bYqeQobj44tgJKmLu3v8LzP4UWOzuf9nz8y8B17h7n/8MtbS0eGdnZ1WDioiUmZntcveWc32skpuJB4FL/+jnlwCHqhFMREQGVklRPwpcZmYfM7PzgC8AHdnGEhGRXgPOqN39pJn9FbAZaAB+4u5PZp5MRESAym4m4u4bAN0tERGJQE8miojknIpaRCTnVNQiIjmnohYRybkBH3gZ0m9q1g3sr/pvPDQTgCOxQwyC8mZLebOlvEM31d2bzvWBTIo6T8yss6+nffJIebOlvNlS3mxo9CEiknMqahGRnKuHor4rdoBBUt5sKW+2lDcDpZ9Ri4gUXT1cUYuIFJqKWkQk5+qmqM3sjp4Dep80s+/HzlMJM/s7M3MzmxA7S3/M7F/M7Ldm9oSZtZvZBbEzna1IBzSb2aVm9oCZdfV8vt4ZO1MlzKzBzHab2brYWQZiZheY2T09n7ddZjY3dqb+1EVRm9nNhHMeZ7n7TOBfI0cakJldCiwCno+dpQJbgU+5+yzgGeBbkfOcoYAHNJ8E/tbdrwCuA76W87y97gS6Yoeo0A+BTe4+HbiKnOeui6IGvgr8k7u/B+DuL0fOU4l/B74B5P5ur7tvcfeTPT/dSTgFKE/+cECzux8Heg9oziV3P+zuj/X8+E1CiUyOm6p/ZnYJsBz4UewsAzGz8cCNwI8B3P24u78eN1X/6qWoLwduMLOHzexBM7s6dqD+mNkK4AV3fzx2liH4C2Bj7BBnOdcBzbkuvl5m1gx8Gng4bpIB/YBwYXE6dpAKTAO6gf/uGdX8yMzGxQ7Vn4oODigCM7sP+Og5PvQdwv/nRwhvI68G/s/MpnnEtYkD5P02cGttE/Wvv7zuvqbnNd8hvG2/u5bZKmDn+LXcv1Mxs/OBe4Gvu/vR2Hn6YmatwMvuvsvM5sfOU4FGYA5wh7s/bGY/BL4JfDdurL6Vpqjd/Za+PmZmXwXSnmJ+xMxOEzZj6a5VvrP1ldfMrgQ+BjxuZhDGCI+Z2TXu/mINI56hvz9fADP7MtAKLIz5D2AfCndAs5mNJJT03e6exs4zgHnACjNbBowGxpvZT939i5Fz9eUgcNDde9+l3EMo6tyql9HHamABgJldDpxHfnbMOoO773H3i9292d2bCZ9Uc2KW9EDMbAnwD8AKd387dp5zKNQBzRb+hf4x0OXu/xY7z0Dc/VvufknP5+sXgPtzXNL0fC0dMLNP9vzSQuCpiJEGVJor6gH8BPiJmf0GOA58OYdXfUX2H8AoYGvPu4Cd7v6VuJHeV8ADmucBXwL2mNmve37t2z1nl0p13AHc3fMP9++BP4+cp196hFxEJOfqZfQhIlJYKmoRkZxTUYuI5JyKWkQk51TUIiI5p6IWEck5FbWISM79P2wKeUpy7q+pAAAAAElFTkSuQmCC\n", + "image/png": "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\n", "text/plain": [ "
" ] @@ -249,37 +249,37 @@ } ], "source": [ - "profile_data = profile.rasterize(0.8)\n", - "shape_data = shape.rasterize(0.8)\n", - "shape_r_data = shape_r.rasterize(0.8)\n", + "profile_data = profile.rasterize(0.3)\n", + "shape_data = shape.rasterize(0.3)\n", + "shape_r_data = shape_r.rasterize(0.3)\n", "\n", "ax = plt.gca()\n", "ax.cla()\n", "ax.axis(\"equal\")\n", "\n", - "plt.plot(shape_data[0],shape_data[1],\"r-\")\n", - "plt.plot(shape_r_data[0],shape_r_data[1],\"r-\")\n", + "plt.plot(shape_data[0],shape_data[1],\"r.\")\n", + "plt.plot(shape_r_data[0],shape_r_data[1],\"r.\")\n", "#plt.plot(profile_data[0],profile_data[1],\"--\")" ] }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 12, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "[]" + "[]" ] }, - "execution_count": 9, + "execution_count": 12, "metadata": {}, "output_type": "execute_result" }, { "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAWoAAAD4CAYAAADFAawfAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4xLjEsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy8QZhcZAAAZl0lEQVR4nO3da2ycZZrm8f/tU+zYcU4+FOSAc8ROGbphwjHinMTVwDD7ZaVeqVujHY2YGc0wzO6sZrtn1FppP6xWu7Oz09K2RkLdPV8GbWvFsDtUgJyaAAMNNAnQDc7rnE8OJLZzjh3Hp3s/lGEaiOOyU1XPW1XXT0IVpyrWBdhXXt/1PO9j7o6IiMRXRegAIiJyfSpqEZGYU1GLiMScilpEJOZU1CIiMVeVj0/a1NTkbW1t+fjUIiIlac+ePQPu3nyt5/JS1G1tbezevTsfn1pEpCSZ2bGpntPoQ0Qk5lTUIiIxp6IWEYk5FbWISMypqEVEYk5FLSIScypqEZGYy8s66rjYta+PD4+dCx2j4L5120103NQYOoYUk5N7YN/W0CkKr20DrHw4dIpplXRRv3VggJ++fSR0jIJyh18ePcvPnr4vdBQpJlv+PXz2EWChkxSWmYo6tB88uY4fPLkudIyC+h/b9/GjXQc5OzjCovqa0HGkGJw/ninpTf8ZNjwbOo1cg2bUJaYrmWDCYefe06GjSLGItmQe258Mm0OmpKIuMcmbG1m6sI6t3adCR5FiEaWhtRMWrwqdRKagoi4xZkZXMsFbBwa4NDwaOo7E3eU+OP6OrqZjTkVdglKdCUbGJ3h9X3/oKBJ3+14BHDp+O3QSuQ4VdQm6c/lCmhrmaPwh04vSsHAFtCZDJ5HrUFGXoMoKY3OylV09fQyPjoeOI3E1fAEOv5G5mrYyW5ZXZFTUJSqVTDA0Ms5bBwZCR5G42r8dJkah46nQSWQaKuoSde/KxcyrrdL4Q6YWvQQNCVjyW6GTyDRU1CWqpqqCjR2t7IxOMzY+ETqOxM3IEBzcCR1PQoVqIO70f6iEdSUTnB8a5ZdHzoaOInFz6DUYHdJqjyKhoi5hD61tpra6QuMP+booDXUL4ZYNoZNIFlTUJayuppKH17awrfsUExMeOo7Exfgo7H8Vbn0cKqtDp5EsqKhLXFdnK6cvXuWj3vOho0hcHP3nzNI87UYsGirqEvdoeytVFca2TzT+kElRGqrrYdUjoZNIllTUJW5+XTX3r25ia/cp3DX+KHsT45m75a3ZBNV1odNIllTUZSCVTHDszBA9py6FjiKh9b4Pg31a7VFkVNRlYNO6Vsxgm1Z/SJSGyhpYszl0EpmBrIrazP6dmXWb2Sdm9r/NrDbfwSR3mufN4a5bFrFVc+ry5p4p6pUPQ63O1Cwm0xa1mS0B/hRY7+6dQCXw7XwHk9zanGyl59Qljg4Mho4ioZz6GM4f09ijCGU7+qgC6sysCpgLfJq/SJIPXckEoPFHWYvSYBWZ9dNSVKYtanc/Cfw1cBz4DLjg7tu/+joze9rMdpvZ7v5+3bA+bpYtmkvnkkbtUixnUTqzE7G+KXQSmaFsRh8Lgd8BVgA3A/Vm9p2vvs7dn3P39e6+vrm5OfdJ5Yalkgk+PH6e0xeHQ0eRQhs4AP2Rxh5FKpvRx0bgiLv3u/so8CJwf35jST6kOjPjj+26qi4/UTrz2P5E2BwyK9kU9XHgXjOba2YGPAZE+Y0l+bC6ZR6rmus1/ihHPVvg5jth/tLQSWQWsplRvwe8AHwAfDz5Z57Lcy7Jk65kgncPn+Xc4EjoKFIoF3rh5B6NPYpYVqs+3P0/uXu7u3e6+3fd/Wq+g0l+pDoTjE84O6PToaNIofS8nHnUkVtFSzsTy8xtS+Zz8/xatnWrqMtGlIbmdmhaHTqJzJKKusyYGV2dCd480M/g1bHQcSTfBgfg2NsaexQ5FXUZSiUTjIxN8Po+rXcvefteAZ9QURc5FXUZWt+2iMX1NVr9UQ6iNCxYDonbQyeRG6CiLkOVFcamda3s6unj6th46DiSL8MX4fDrmTcRzUKnkRugoi5TXZ0JLl8d4xcHz4SOIvlyYDuMj+jIrRKgoi5T969azLw5Vbr1aSmL0lDfAsvuDp1EbpCKukzNqark0Y4WdkSnGRufCB1Hcm30ChzYAe2PQ0Vl6DRyg1TUZSyVTHB2cIT3j54LHUVy7dAuGB3UJpcSoaIuYw/d2sycqgrdo7oURWmonQ9tD4ROIjmgoi5jc2uqeHBtM9t0QnlpGR+F/a/C2hRU1YROIzmgoi5zqWSCzy4M8+veC6GjSK4cexuunNMmlxKioi5zj3W0UFVh2vxSSqI0VNXBqsdCJ5EcUVGXuQVza7hv1WK2fqLxR0mYmIBoC6zZCDVzQ6eRHFFRC5uTCY4MDHKg73LoKHKjTu6Gy6e02qPEqKiFrnWtmME2bX4pflEaKqphzebQSSSHVNRCS2Mtdy5fqDl1sXPPFPXKh6BuQeg0kkMqagEyqz+6P73IibNDoaPIbJ3uhnNHdG+PEqSiFiBzliKgzS/FLEoDppPGS5CKWgBYvnguHTc16iZNxSxKw/L7oKEldBLJMRW1fCGVTLDn+Dn6Lg2HjiIzdeYQ9HVrk0uJUlHLF1KdCdxhx14dfFt0erZkHjs0ny5FKmr5wtrWBlY01Wv8UYyiNNz0zcyxW1JyVNTyBTOjK5ngnUNnuDA0GjqOZOvip9D7vq6mS5iKWr6kK9nK2ITz8x6NP4pGz8uZR+1GLFkqavmSbyxdQKKxVsv0ikmUhqa10Hxr6CSSJypq+ZKKCqMr2cob+/sZGhkLHUemM3QWjr6l1R4lTkUtX9PVmWB4dII39/eHjiLT2fcq+LiKusSpqOVr7m5bxMK51Vr9UQyiNMxfllnxISVLRS1fU1VZwcaOVn4e9TEyphPKY+vqZTj0WubeHmah00geqajlmlKdCS5dHeMXhwZCR5GpHNwB41c19igDKmq5pg2rm6ivqWRbt5bpxVaUhrlNsPze0Ekkz1TUck211ZU80t7Cjr2nGJ/QEV2xMzoM+7dl7pRXURk6jeSZilqmlOpMMHB5hD3HzoWOIl915A0YuaxNLmVCRS1TevjWFmqqKrT6I46iNMxphBUPhk4iBZBVUZvZAjN7wcx6zCwys/vyHUzCa5hTxQOrm9jWrRPKY2V8DPa9Amu7oKomdBopgGyvqH8IbHX3duAbQJS/SBInXZ0JTp6/wicnL4aOIp87/g4MndFqjzIybVGbWSPwIPATAHcfcffz+Q4m8bCxo5XKCmNr92eho8jnojRU1cLqjaGTSIFkc0W9EugH/t7MPjSzH5tZ/VdfZGZPm9luM9vd36+tx6ViUX0N96xYpGV6ceGeOSRg9Uao+dq3oZSobIq6CrgT+Dt3vwMYBL731Re5+3Puvt7d1zc3N+c4poSU6kxwsO8yB/suhY4in34AF09q7FFmsinqXqDX3d+b/PgFMsUtZWLzuswJ5Vr9EQNRGiqqMm8kStmYtqjd/RRwwsw+v9ntY8DevKaSWEnMr+WbyxZo/BGaO+x9CdoegLqFodNIAWW76uMZ4Hkz+zXwTeC/5C+SxFGqM8HHJy/Qe24odJTy1d8DZw9p7FGGsipqd/9ocv58u7v/K3fXVrUy05XMjD+266o6nCgNWGbbuJQV7UyUrKxoqqc9MY+tOqIrnCgNy+6BeYnQSaTAVNSStc3JBO8fPUv/pauho5Sfc0fh1K910niZUlFL1lLJBO6wM9L4o+CiLZnHdhV1OVJRS9Y6bprH8kVztUwvhCgNidtg0YrQSSQAFbVkzcxIdSb4xaEBLg6Pho5TPi6dghPv6ZamZUxFLTPSlUwwOu7s6ukLHaV89LwMuJbllTEVtczIHcsW0DJvjsYfhdSzBRatgub20EkkEBW1zEhFhbE52crr+/oZHh0PHaf0XTkHR97MXE3rpPGypaKWGUslb+LK6Dhv7tddEvNu/zaYGNN8usypqGXG7lm5iPl11dr8UghRGhqXwM13hE4iAamoZcaqKyt4rKOFnXtPMzo+ETpO6RoZhIM7J08a17dqOdP/fZmVVDLBxeEx3j18JnSU0nVwJ4wNa7WHqKhldh5c20xddaVWf+RTtAXqFsHy+0MnkcBU1DIrtdWVPNLezPa9p5mY0AnlOTc2Avu3wq2PQ2VV6DQSmIpaZq0rmaD/0lU+PKG73ubckTfh6kVYp9UeoqKWG/BIewvVlabxRz5EL0FNA6x4KHQSiQEVtcxaY201G1Y3sbX7FO4af+TMxHhm2/iazVBdGzqNxICKWm5IKpngxNkr7P3sYugopeP4uzA0oNUe8gUVtdyQjetaqTDYpvFH7kRpqJwDazaFTiIxoaKWG9LUMIe72hbphPJccc/chGnVozBnXug0EhMqarlhqc4E+05f4nD/5dBRit9nH8GFExp7yJeoqOWGbZ48oVxX1TkQpcEq4dZvhU4iMaKilhu2ZEEdty+dr5s05UKUhrYNMHdR6CQSIypqyYmuZIJfnTjPp+evhI5SvPr3wcB+3dJUvkZFLTmR6syMP7brqnr2opcyj+1PhM0hsaOilpxY1dzAmpYGzalvRLQFlt4FjTeHTiIxo6KWnEl1JnjvyBnODo6EjlJ8zh/PrPjQag+5BhW15ExXMsGEw869uqqesWhL5rH9ybA5JJZU1JIzyZsbWbKgTqs/ZiNKQ0sSFq8KnURiSEUtOWNmpDoTvHVggEvDo6HjFI/LfXD8HY09ZEoqasmpVGeCkfEJdu3TCeVZ63kZcOjQ2EOuTUUtOXXn8oU0Ncxhm8Yf2evZAgtXQGtn6CQSUypqyanKCmNzspVdPX0Mj46HjhN/V87D4TcyYw+z0GkkplTUknOpZIKhkXHeOjAQOkr8HdgOE6PajSjXpaKWnLt35WLm1VZp9Uc2opegIQFLfit0EomxrIvazCrN7EMz25LPQFL8aqoq2NjRys7oNGPjE6HjxNfIEBzYmXkTsULXTDK1mXx1PAtE+QoipaUrmeD80Ci/PHI2dJT4OvQajF3RsjyZVlZFbWZLgSeAH+c3jpSKh9Y2U1tdofHH9URpqFsIt2wInURiLtsr6r8F/gKY8udYM3vazHab2e7+fq2hLXd1NZU8vLaFbd2nmJjQCeVfMzYC+1+FWx+HyurQaSTmpi1qM3sS6HP3Pdd7nbs/5+7r3X19c3NzzgJK8Up1Jjh98Sof9Z4PHSV+jv4zDF/Q2EOyks0V9QbgKTM7CvwMeNTM/iGvqaQkPNLeQlWFafPLtfRsgep6WPlI6CRSBKYtanf/vrsvdfc24NvAa+7+nbwnk6I3v66a+1c3se2TU7hr/PGFifHM3fLWbILq2tBppAhoTZDkVSqZ4OiZIfadvhQ6Snz0vg+DfRp7SNZmVNTu/rq7684xkrVN61oxg62faPzxhSgNlTWwZnPoJFIkdEUtedU8bw533bJIRf0598xuxJWPQG1j6DRSJFTUknddnQl6Tl3i2JnB0FHCO/Vx5tgtjT1kBlTUknddyVYArf6AzNjDKjLrp0WypKKWvFu6cC6dSxp5VeOPTFHfsgHqF4dOIkVERS0FkUom+PD4eU5fHA4dJZyBA9Af6QBbmTEVtRREqjMBwPZyHn9E6cyjjtySGVJRS0GsbpnHquZ6tnWfDh0lnJ4tmftOz18aOokUGRW1FExXMsE7h89wfmgkdJTCu9ALJ/do7CGzoqKWgkl1JhifcHZGfaGjFF7Py5lHHbkls6CiloK5bcl8bp5fW56bX6I0NHdA0+rQSaQIqailYMyMrs4Ebx7oZ/DqWOg4hTM4AMfe1iYXmTUVtRRUKplgZGyCN/aX0eES+14Fn1BRy6ypqKWg1rctYnF9TXmNP6I0LFgOidtCJ5EipaKWgqqsMData+W1nj6ujo2HjpN/wxfh8K7Mm4hmodNIkVJRS8F1dSa4fHWMXxw8EzpK/h3YDuMjGnvIDVFRS8Hdv2ox8+ZUlcf4I0pDQyssvTt0EiliKmopuDlVlTza0cKO6DRj41MebF/8Rq/AgR3Q/gRU6FtNZk9fPRJEVzLB2cERdh87FzpK/hx+HUYHtRtRbpiKWoJ4aG0zc6oqSnv8EaWhdj60PRA6iRQ5FbUEUT+nigfXNrOtu0RPKB8fhX2vwNpvQVVN6DRS5FTUEkwqmeCzC8P8qvdC6Ci5d+xtuHJOqz0kJ1TUEsxjHS1UVlhpHtEVpaGqDlY9GjqJlAAVtQSzYG4N961czLZPSmz8MTEB0RZYsxFq5oZOIyVARS1BdXUmODwwyMG+y6Gj5M7JPXD5lG5pKjmjopaguta1YkZprf6IXoKKalizOXQSKREqagmqpbGWO5cvZGupzKndM/PplQ9B3YLQaaREqKgluFQyQfenFzlxdih0lBt3uhvOHdFqD8kpFbUE15XMnFBeEqs/ojRgcOvjoZNICVFRS3DLF8+l46bG0phTR2lYfh80tIROIiVERS2xkEom2HP8HH2XhkNHmb0zh6CvW2MPyTkVtcRCqjOBO+zYezp0lNnr2ZJ57NBNmCS3VNQSC2tbG1jRVF/c448oDTd9M3PslkgOqaglFsyMrmSCdw6d4cLQaOg4M3fxU+h9X2MPyQsVtcRGqjPB2ITz854iHH/0vJx51G5EyQMVtcTG7Uvmk2isLc7xR/QSNK2F5rWhk0gJmraozWyZme0ys8jMus3s2UIEk/JTUWF0JVt580A/QyNjoeNkb+gsHH1bYw/Jm2yuqMeAP3f3DuBe4I/NbF1+Y0m56upMMDw6wZv7+0NHyd6+V8HHVdSSN9MWtbt/5u4fTP76EhABS/IdTMrT3W2LWDi3urjGHz1bYP6yzIoPkTyY0YzazNqAO4D3rvHc02a228x29/cX0dWQxEpVZQXtiUZOnr8SOkr2zp+AxG1gFjqJlKisi9rMGoB/BP7M3S9+9Xl3f87d17v7+ubm5lxmlDJTnH1XlKGlSGRV1GZWTaakn3f3F/MbSUREflM2qz4M+AkQufvf5D+SiIj8pmyuqDcA3wUeNbOPJv/RPRxFRAqkaroXuPtbaAAnIhKMdiaKiMScilpEJOZU1CIiMaeiFhGJORW1iEjMqahFRGJORS0iEnMqahGRmFNRi4jEnIpaRCTmVNQiIjGnohYRiTkVtYhIzKmoRURiTkUtIhJzKmoRkZhTUYuIxJyKWkQk5lTUIiIxp6IWEYk5FbWISMypqEVEYk5FLSIScypqEZGYU1GLiMScilpEJOZU1CIiMaeiFhGJORW1iEjMqahFRGJORS0iEnMqahGRmFNRi4jEnIpaRCTmVNQiIjGXVVGbWcrM9pnZQTP7Xr5DiYjIv5i2qM2sEvgR8C1gHfBvzGxdvoOJiEhGVRavuRs46O6HAczsZ8DvAHvzGSwX/u71Q7z4QW/oGDJDveeu0LmkMXSMmTnyBvzontApZKbW/x7c8wehU0wrm6JeApz4jY97ga99RZrZ08DTAMuXL89JuBvV1FDDmtaG0DFkhta0NtCVTISOkb17/xAObA+dQmajvil0gqyYu1//BWb/Guhy99+f/Pi7wN3u/sxUf2b9+vW+e/funAYVESllZrbH3ddf67ls3kzsBZb9xsdLgU9zEUxERKaXTVG/D6wxsxVmVgN8G3gpv7FERORz086o3X3MzP4E2AZUAj919+68JxMRESC7NxNx91eAV/KcRURErkE7E0VEYk5FLSIScypqEZGYU1GLiMTctBteZvVJzfqBYzn/xLPTBAyEDjEDyptfyptfyjt7t7h787WeyEtRx4mZ7Z5qt08cKW9+KW9+KW9+aPQhIhJzKmoRkZgrh6J+LnSAGVLe/FLe/FLePCj5GbWISLErhytqEZGipqIWEYm5silqM3tm8oDebjP7b6HzZMPM/oOZuZnF+hgKM/vvZtZjZr82s/9rZgtCZ/qqYjqg2cyWmdkuM4smv16fDZ0pG2ZWaWYfmtmW0FmmY2YLzOyFya/byMzuC53pesqiqM3sETLnPN7u7kngrwNHmpaZLQM2AcdDZ8nCDqDT3W8H9gPfD5znS4rwgOYx4M/dvQO4F/jjmOf93LNAFDpEln4IbHX3duAbxDx3WRQ18EfAf3X3qwDu3hc4Tzb+J/AXQOzf7XX37e4+Nvnhu2ROAYqTLw5odvcR4PMDmmPJ3T9z9w8mf32JTIksCZvq+sxsKfAE8OPQWaZjZo3Ag8BPANx9xN3Ph011feVS1GuBB8zsPTN7w8zuCh3oeszsKeCku/8qdJZZ+D3g1dAhvuJaBzTHuvg+Z2ZtwB3Ae2GTTOtvyVxYTIQOkoWVQD/w95Ojmh+bWX3oUNeT1cEBxcDMdgLXOrr6r8j8ey4k82PkXcD/MbOVHnBt4jR5/xLYXNhE13e9vO7+T5Ov+SsyP7Y/X8hsWbBr/F7sf1IxswbgH4E/c/eLofNMxcyeBPrcfY+ZPRw6TxaqgDuBZ9z9PTP7IfA94AdhY02tZIra3TdO9ZyZ/RHw4mQx/9LMJsjcjKW/UPm+aqq8ZnYbsAL4lZlBZozwgZnd7e6nChjxS6733xfAzH4XeBJ4LORfgFMougOazayaTEk/7+4vhs4zjQ3AU2b2OFALNJrZP7j7dwLnmkov0Ovun/+U8gKZoo6tchl9/D/gUQAzWwvUEJ87Zn2Ju3/s7i3u3ububWS+qO4MWdLTMbMU8B+Bp9x9KHSeayiqA5ot8zf0T4DI3f8mdJ7puPv33X3p5Nfrt4HXYlzSTH4vnTCzWyd/6zFgb8BI0yqZK+pp/BT4qZl9AowAvxvDq75i9r+AOcCOyZ8C3nX3Pwwb6V8U4QHNG4DvAh+b2UeTv/eXk2eXSm48Azw/+Rf3YeDfBs5zXdpCLiISc+Uy+hARKVoqahGRmFNRi4jEnIpaRCTmVNQiIjGnohYRiTkVtYhIzP1/vcpk4tcYa+QAAAAASUVORK5CYII=\n", + "image/png": "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\n", "text/plain": [ "
" ] @@ -298,9 +298,9 @@ "ax.axis(\"equal\")\n", "\n", "plt.plot(profile_data[0][:h],\n", - " profile_data[1][:h], \"-\")\n", + " profile_data[1][:h], \".\")\n", "plt.plot(profile_data[0][h:],\n", - " profile_data[1][h:], \"-\")" + " profile_data[1][h:], \".\")" ] }, { @@ -312,7 +312,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 1, "metadata": {}, "outputs": [], "source": [ @@ -323,7 +323,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ @@ -338,7 +338,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 3, "metadata": {}, "outputs": [], "source": [ @@ -347,22 +347,22 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 4, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "[]" + "[]" ] }, - "execution_count": 13, + "execution_count": 4, "metadata": {}, "output_type": "execute_result" }, { "data": { - "image/png": "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\n", + "image/png": "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\n", "text/plain": [ "
" ] @@ -383,12 +383,12 @@ "ax.grid(True)\n", "ax.set(title=\"single V Groove Butt Weld {}° groove angle\".format(alpha.value), xlabel=\"millimeter\", ylabel=\"millimeter\")\n", "\n", - "plt.plot(profile_data[0],profile_data[1],\"-\")" + "plt.plot(profile_data[0],profile_data[1],\".\")" ] }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 5, "metadata": {}, "outputs": [], "source": [ @@ -403,7 +403,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 6, "metadata": {}, "outputs": [], "source": [ @@ -412,22 +412,22 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 7, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "[]" + "[]" ] }, - "execution_count": 16, + "execution_count": 7, "metadata": {}, "output_type": "execute_result" }, { "data": { - "image/png": "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\n", + "image/png": "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\n", "text/plain": [ "
" ] @@ -449,12 +449,12 @@ "ax.grid(True)\n", "ax.set(title=\"single V Groove Butt Weld {}° groove angle\".format(alpha.value), xlabel=\"millimeter\", ylabel=\"millimeter\")\n", "\n", - "plt.plot(profile_data[0],profile_data[1],\"x\")" + "plt.plot(profile_data[0],profile_data[1],\".\")" ] }, { "cell_type": "code", - "execution_count": 21, + "execution_count": 8, "metadata": {}, "outputs": [], "source": [ @@ -468,7 +468,7 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": 9, "metadata": {}, "outputs": [], "source": [ @@ -477,22 +477,22 @@ }, { "cell_type": "code", - "execution_count": 23, + "execution_count": 10, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "[]" + "[]" ] }, - "execution_count": 23, + "execution_count": 10, "metadata": {}, "output_type": "execute_result" }, { "data": { - "image/png": "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\n", + "image/png": "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\n", "text/plain": [ "
" ] @@ -514,28 +514,7 @@ "ax.grid(True)\n", "ax.set(title=\"single U Groove Butt Weld {}° groove angle\".format(beta.value), xlabel=\"millimeter\", ylabel=\"millimeter\")\n", "\n", - "plt.plot(profile_data[0],profile_data[1],\"x\")" - ] - }, - { - "cell_type": "code", - "execution_count": 20, - "metadata": {}, - "outputs": [ - { - "ename": "TypeError", - "evalue": "__init__() missing 1 required positional argument: 'points'", - "output_type": "error", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", - "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mgeo\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mArcSegment\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", - "\u001b[0;31mTypeError\u001b[0m: __init__() missing 1 required positional argument: 'points'" - ] - } - ], - "source": [ - "geo.ArcSegment()" + "plt.plot(profile_data[0],profile_data[1],\".\")" ] } ], diff --git a/mypackage/all_groove.py b/mypackage/all_groove.py index cd6ab83..6934a8b 100644 --- a/mypackage/all_groove.py +++ b/mypackage/all_groove.py @@ -2,10 +2,8 @@ from astropy.units import Quantity import numpy as np -import copy import mypackage.geometry as geo -import mypackage.point_cloud_generator as pcg def singleVGrooveButtWeld(d, width_default=Quantity(5, unit="millimeter")): @@ -44,16 +42,13 @@ def singleVGrooveButtWeld(d, width_default=Quantity(5, unit="millimeter")): top = geo.LineSegment([[-s, -width], [t, t]]) segment_list.append(top) - shape = geo.Shape2D(segment_list) + shape = geo.Shape(segment_list) - shape.translate([-b / 2, 0]) - shape_r = copy.deepcopy(shape) - if b != 0: - shape_r.reflect([-b / 2, 0]) - else: - shape_r.reflect([-1, 0]) + shape = shape.translate([-b / 2, 0]) + # y Achse als Spiegelachse + shape_r = shape.reflect_across_line([0, 0], [0, 1]) - profile = pcg.Profile([shape, shape_r]) + profile = geo.Profile([shape, shape_r]) return profile @@ -95,8 +90,8 @@ def singleUGrooveButtWeld(d, width_default=Quantity(15, unit="millimeter")): y = R * np.sin(beta) # m = [0,c+R] Kreismittelpunkt # => [-x,c+R-y] ist der nächste Punkt - groove_face_arc = geo.ArcSegment([[0, -x, 0], - [c, c + R - y, c + R]], False) + groove_face_arc = geo.ArcSegment([[0, -x, 0], [c, c + R - y, c + R]], + False) segment_list.append(groove_face_arc) s = np.tan(beta) * (t - (c + R - y)) @@ -106,15 +101,12 @@ def singleUGrooveButtWeld(d, width_default=Quantity(15, unit="millimeter")): top = geo.LineSegment([[-x - s, -width], [t, t]]) segment_list.append(top) - shape = geo.Shape2D(segment_list) + shape = geo.Shape(segment_list) - shape.translate([-b / 2, 0]) - shape_r = copy.deepcopy(shape) - if b != 0: - shape_r.reflect([-b / 2, 0]) - else: - shape_r.reflect([-1, 0]) + shape = shape.translate([-b / 2, 0]) + # y Achse als Spiegelachse + shape_r = shape.reflect_across_line([0, 0], [0, 1]) - profile = pcg.Profile([shape, shape_r]) + profile = geo.Profile([shape, shape_r]) return profile From 943cb8867ee20f7ece8dcaeaebcc6a91bd78435a Mon Sep 17 00:00:00 2001 From: mnagel Date: Wed, 12 Feb 2020 11:26:04 +0100 Subject: [PATCH 170/177] Changed input of classes from dictionary to single values. Now the input can be given with '**dict' . --- Weld_tester.ipynb | 60 +++++++++++++++++++++++++---------------- mypackage/all_groove.py | 54 +++++++++++++++++++------------------ 2 files changed, 65 insertions(+), 49 deletions(-) diff --git a/Weld_tester.ipynb b/Weld_tester.ipynb index 54bf971..2591989 100644 --- a/Weld_tester.ipynb +++ b/Weld_tester.ipynb @@ -331,7 +331,7 @@ "# in a dictionary\n", "t = Quantity(0.009,unit=\"meter\")\n", "alpha = Quantity(40, unit=\"deg\")\n", - "b = Quantity(0, unit=\"centimeter\")\n", + "b = Quantity(0.2, unit=\"centimeter\")\n", "c = Quantity(1, unit=\"millimeter\")\n", "v_naht_dict = dict(t=t, alpha=alpha, b=b, c=c)" ] @@ -342,27 +342,27 @@ "metadata": {}, "outputs": [], "source": [ - "profile = singleVGrooveButtWeld(v_naht_dict)" + "profile = singleVGrooveButtWeld(**v_naht_dict)" ] }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 5, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "[]" + "[]" ] }, - "execution_count": 4, + "execution_count": 5, "metadata": {}, "output_type": "execute_result" }, { "data": { - "image/png": "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\n", + "image/png": "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\n", "text/plain": [ "
" ] @@ -378,8 +378,8 @@ "\n", "ax = plt.gca()\n", "ax.cla()\n", - "#ax.axis(\"equal\")\n", - "ax.axis([-7, +7, -1, 20])\n", + "ax.axis(\"equal\")\n", + "#ax.axis([-7, +7, -1, 20])\n", "ax.grid(True)\n", "ax.set(title=\"single V Groove Butt Weld {}° groove angle\".format(alpha.value), xlabel=\"millimeter\", ylabel=\"millimeter\")\n", "\n", @@ -407,27 +407,27 @@ "metadata": {}, "outputs": [], "source": [ - "profile = singleVGrooveButtWeld(v_naht_dict)" + "profile = singleVGrooveButtWeld(**v_naht_dict)" ] }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 6, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "[]" + "[]" ] }, - "execution_count": 7, + "execution_count": 6, "metadata": {}, "output_type": "execute_result" }, { "data": { - "image/png": "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\n", + "image/png": "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\n", "text/plain": [ "
" ] @@ -443,8 +443,8 @@ "\n", "ax = plt.gca()\n", "ax.cla()\n", - "#ax.axis(\"equal\")\n", - "ax.axis([-7, +7, -1, 20])\n", + "ax.axis(\"equal\")\n", + "#ax.axis([-7, +7, -1, 20])\n", "#[xmin, xmax, ymin, ymax]\n", "ax.grid(True)\n", "ax.set(title=\"single V Groove Butt Weld {}° groove angle\".format(alpha.value), xlabel=\"millimeter\", ylabel=\"millimeter\")\n", @@ -454,7 +454,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 7, "metadata": {}, "outputs": [], "source": [ @@ -468,31 +468,31 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 8, "metadata": {}, "outputs": [], "source": [ - "profile = singleUGrooveButtWeld(u_naht_dict)" + "profile = singleUGrooveButtWeld(**u_naht_dict)" ] }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 12, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "[]" + "[]" ] }, - "execution_count": 10, + "execution_count": 12, "metadata": {}, "output_type": "execute_result" }, { "data": { - "image/png": "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\n", + "image/png": "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\n", "text/plain": [ "
" ] @@ -504,7 +504,7 @@ } ], "source": [ - "profile_data = profile.rasterize(0.2)\n", + "profile_data = profile.rasterize(0.5)\n", "\n", "ax = plt.gca()\n", "ax.cla()\n", @@ -516,6 +516,20 @@ "\n", "plt.plot(profile_data[0],profile_data[1],\".\")" ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { diff --git a/mypackage/all_groove.py b/mypackage/all_groove.py index 6934a8b..3911e5b 100644 --- a/mypackage/all_groove.py +++ b/mypackage/all_groove.py @@ -6,24 +6,25 @@ import mypackage.geometry as geo -def singleVGrooveButtWeld(d, width_default=Quantity(5, unit="millimeter")): +def singleVGrooveButtWeld( + t, alpha, b, c, width_default=Quantity(5, unit="millimeter") +): """ The calculation of a Single-V Groove Butt Weld. - Required variables in the dictionary are in Quantity(astropy): - t: the workpiece thickness - alpha: the groove angle - b: the root opening - c: the root face + Required variables are in Quantity(astropy): - - :param d: input dictionary with astropy Quantity + :param t: the workpiece thickness + :param alpha: the groove angle + :param b: the root opening + :param c: the root face :param width_default: the width of the workpiece + :return: point_could_generator.Profile """ - t = d["t"].to_value("millimeter") - alpha = d["alpha"].to_value("rad") - b = d["b"].to_value("millimeter") - c = d["c"].to_value("millimeter") + t = t.to_value("millimeter") + alpha = alpha.to_value("rad") + b = b.to_value("millimeter") + c = c.to_value("millimeter") width = width_default.to_value("millimeter") segment_list = [] @@ -53,27 +54,28 @@ def singleVGrooveButtWeld(d, width_default=Quantity(5, unit="millimeter")): return profile -def singleUGrooveButtWeld(d, width_default=Quantity(15, unit="millimeter")): +def singleUGrooveButtWeld( + t, beta, R, b, c, width_default=Quantity(15, unit="millimeter") +): """ The calculation of a Single-U Groove Butt Weld. - Required variables in the dictionary are in Quantity(astropy): - t: the workpiece thickness - beta: the bevel angle - R: radius - b: the root opening - c: the root face + Required variables are in Quantity(astropy): - - :param d: input dictionary with astropy Quantity + :param t: the workpiece thickness + :param beta: the bevel angle + :param R: radius + :param b: the root opening + :param c: the root face :param width_default: the width of the workpiece + :return: point_could_generator.Profile """ - t = d["t"].to_value("millimeter") - beta = d["beta"].to_value("rad") - R = d["R"].to_value("millimeter") - b = d["b"].to_value("millimeter") - c = d["c"].to_value("millimeter") + t = t.to_value("millimeter") + beta = beta.to_value("rad") + R = R.to_value("millimeter") + b = b.to_value("millimeter") + c = c.to_value("millimeter") width = width_default.to_value("millimeter") segment_list = [] From e11061609bd616ec76f1335c15084c782f23341b Mon Sep 17 00:00:00 2001 From: mnagel Date: Wed, 12 Feb 2020 11:41:11 +0100 Subject: [PATCH 171/177] pydocstyle changes --- mypackage/all_groove.py | 35 +++++++++++++++-------------------- 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/mypackage/all_groove.py b/mypackage/all_groove.py index 3911e5b..589ec3a 100644 --- a/mypackage/all_groove.py +++ b/mypackage/all_groove.py @@ -1,4 +1,4 @@ -"""provides the calculation of all Groove-Types""" +"""provides the calculation of all Groove-Types.""" from astropy.units import Quantity import numpy as np @@ -10,15 +10,13 @@ def singleVGrooveButtWeld( t, alpha, b, c, width_default=Quantity(5, unit="millimeter") ): """ - The calculation of a Single-V Groove Butt Weld. - Required variables are in Quantity(astropy): - - :param t: the workpiece thickness - :param alpha: the groove angle - :param b: the root opening - :param c: the root face - :param width_default: the width of the workpiece + Calculation of a Single-V Groove Butt Weld. + :param t: the workpiece thickness, as Astropy unit + :param alpha: the groove angle, as Astropy unit + :param b: the root opening, as Astropy unit + :param c: the root face, as Astropy unit + :param width_default: the width of the workpiece, as Astropy unit :return: point_could_generator.Profile """ t = t.to_value("millimeter") @@ -58,19 +56,16 @@ def singleUGrooveButtWeld( t, beta, R, b, c, width_default=Quantity(15, unit="millimeter") ): """ - The calculation of a Single-U Groove Butt Weld. - Required variables are in Quantity(astropy): - - :param t: the workpiece thickness - :param beta: the bevel angle - :param R: radius - :param b: the root opening - :param c: the root face - :param width_default: the width of the workpiece - + Calculation of a Single-U Groove Butt Weld. + + :param t: the workpiece thickness, as Astropy unit + :param beta: the bevel angle, as Astropy unit + :param R: radius, as Astropy unit + :param b: the root opening, as Astropy unit + :param c: the root face, as Astropy unit + :param width_default: the width of the workpiece, as Astropy unit :return: point_could_generator.Profile """ - t = t.to_value("millimeter") beta = beta.to_value("rad") R = R.to_value("millimeter") From aec325caa3dc7029052b0bf06fef37a0b9047f61 Mon Sep 17 00:00:00 2001 From: mnagel Date: Wed, 12 Feb 2020 14:47:54 +0100 Subject: [PATCH 172/177] imperative mood test?! --- mypackage/all_groove.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mypackage/all_groove.py b/mypackage/all_groove.py index 589ec3a..6d55603 100644 --- a/mypackage/all_groove.py +++ b/mypackage/all_groove.py @@ -10,7 +10,7 @@ def singleVGrooveButtWeld( t, alpha, b, c, width_default=Quantity(5, unit="millimeter") ): """ - Calculation of a Single-V Groove Butt Weld. + Calculates a Single-V Groove Butt Weld. :param t: the workpiece thickness, as Astropy unit :param alpha: the groove angle, as Astropy unit @@ -56,7 +56,7 @@ def singleUGrooveButtWeld( t, beta, R, b, c, width_default=Quantity(15, unit="millimeter") ): """ - Calculation of a Single-U Groove Butt Weld. + Calculates a Single-U Groove Butt Weld. :param t: the workpiece thickness, as Astropy unit :param beta: the bevel angle, as Astropy unit From df74fc49c0f1e6bd88836e3045c7a80af6b410ca Mon Sep 17 00:00:00 2001 From: mnagel Date: Wed, 12 Feb 2020 14:51:48 +0100 Subject: [PATCH 173/177] format notebook with black --- Weld_tester.ipynb | 98 +++++++++++++++++++++++++++-------------------- 1 file changed, 57 insertions(+), 41 deletions(-) diff --git a/Weld_tester.ipynb b/Weld_tester.ipynb index 2591989..2b65d75 100644 --- a/Weld_tester.ipynb +++ b/Weld_tester.ipynb @@ -58,7 +58,7 @@ } ], "source": [ - "seg = geo.LineSegment([[2,0],[3,1]])\n", + "seg = geo.LineSegment([[2, 0], [3, 1]])\n", "seg_data = seg.rasterize(0.15)\n", "print(seg_data)\n", "plt.plot(seg_data[0], seg_data[1])" @@ -86,7 +86,7 @@ "source": [ "# Values for the Single V Groove Butt Weld\n", "# in a dictionary\n", - "t = Quantity(0.009,unit=\"meter\")\n", + "t = Quantity(0.009, unit=\"meter\")\n", "alpha = Quantity(40, unit=\"deg\")\n", "b = Quantity(0.2, unit=\"centimeter\")\n", "c = Quantity(1, unit=\"millimeter\")\n", @@ -102,15 +102,28 @@ "outputs": [], "source": [ "# V-naht berechnungen und erstellen der einzelnen Teilsegmente\n", - "bottom = geo.LineSegment([[-width_default.to_value(\"millimeter\"),0],[0,0]])\n", - "root_face = geo.LineSegment([[0,0],[0,v_naht_dict[\"b\"].to_value(\"millimeter\")]])\n", - "alpha_halbe = v_naht_dict[\"alpha\"].to_value(\"deg\")/2\n", + "bottom = geo.LineSegment([[-width_default.to_value(\"millimeter\"), 0], [0, 0]])\n", + "root_face = geo.LineSegment([[0, 0], [0, v_naht_dict[\"b\"].to_value(\"millimeter\")]])\n", + "alpha_halbe = v_naht_dict[\"alpha\"].to_value(\"deg\") / 2\n", "s = np.tan(alpha_halbe)\n", - "groove_face = geo.LineSegment([[0,-s],[v_naht_dict[\"b\"].to_value(\"millimeter\"),\n", - " v_naht_dict[\"t\"].to_value(\"millimeter\")]])\n", - "top = geo.LineSegment([[-s, -width_default.to_value(\"millimeter\")],\n", - " [v_naht_dict[\"t\"].to_value(\"millimeter\"),\n", - " v_naht_dict[\"t\"].to_value(\"millimeter\")]])" + "groove_face = geo.LineSegment(\n", + " [\n", + " [0, -s],\n", + " [\n", + " v_naht_dict[\"b\"].to_value(\"millimeter\"),\n", + " v_naht_dict[\"t\"].to_value(\"millimeter\"),\n", + " ],\n", + " ]\n", + ")\n", + "top = geo.LineSegment(\n", + " [\n", + " [-s, -width_default.to_value(\"millimeter\")],\n", + " [\n", + " v_naht_dict[\"t\"].to_value(\"millimeter\"),\n", + " v_naht_dict[\"t\"].to_value(\"millimeter\"),\n", + " ],\n", + " ]\n", + ")" ] }, { @@ -156,7 +169,7 @@ "ax.cla()\n", "ax.axis(\"equal\")\n", "\n", - "plt.plot(shape_data[0],shape_data[1],\"r-\")" + "plt.plot(shape_data[0], shape_data[1], \"r-\")" ] }, { @@ -196,7 +209,7 @@ ], "source": [ "# shape verschieben und verdoppeln\n", - "b_halbe = v_naht_dict[\"b\"].to_value(\"millimeter\")/2\n", + "b_halbe = v_naht_dict[\"b\"].to_value(\"millimeter\") / 2\n", "print(b_halbe)\n", "shape = shape.translate([-b_halbe, 0])\n", "shape_r = shape.reflect_across_line([0, 0], [0, 1])\n", @@ -207,7 +220,7 @@ "ax.cla()\n", "ax.axis(\"equal\")\n", "\n", - "plt.plot(shape_r_data[0],shape_r_data[1],\"r-\")" + "plt.plot(shape_r_data[0], shape_r_data[1], \"r-\")" ] }, { @@ -257,9 +270,9 @@ "ax.cla()\n", "ax.axis(\"equal\")\n", "\n", - "plt.plot(shape_data[0],shape_data[1],\"r.\")\n", - "plt.plot(shape_r_data[0],shape_r_data[1],\"r.\")\n", - "#plt.plot(profile_data[0],profile_data[1],\"--\")" + "plt.plot(shape_data[0], shape_data[1], \"r.\")\n", + "plt.plot(shape_r_data[0], shape_r_data[1], \"r.\")\n", + "# plt.plot(profile_data[0],profile_data[1],\"--\")" ] }, { @@ -291,16 +304,14 @@ } ], "source": [ - "h = len(profile_data[0])//2\n", + "h = len(profile_data[0]) // 2\n", "\n", "ax = plt.gca()\n", "ax.cla()\n", "ax.axis(\"equal\")\n", "\n", - "plt.plot(profile_data[0][:h],\n", - " profile_data[1][:h], \".\")\n", - "plt.plot(profile_data[0][h:],\n", - " profile_data[1][h:], \".\")" + "plt.plot(profile_data[0][:h], profile_data[1][:h], \".\")\n", + "plt.plot(profile_data[0][h:], profile_data[1][h:], \".\")" ] }, { @@ -329,7 +340,7 @@ "source": [ "# Values for the Single V Groove Butt Weld\n", "# in a dictionary\n", - "t = Quantity(0.009,unit=\"meter\")\n", + "t = Quantity(0.009, unit=\"meter\")\n", "alpha = Quantity(40, unit=\"deg\")\n", "b = Quantity(0.2, unit=\"centimeter\")\n", "c = Quantity(1, unit=\"millimeter\")\n", @@ -379,11 +390,15 @@ "ax = plt.gca()\n", "ax.cla()\n", "ax.axis(\"equal\")\n", - "#ax.axis([-7, +7, -1, 20])\n", + "# ax.axis([-7, +7, -1, 20])\n", "ax.grid(True)\n", - "ax.set(title=\"single V Groove Butt Weld {}° groove angle\".format(alpha.value), xlabel=\"millimeter\", ylabel=\"millimeter\")\n", + "ax.set(\n", + " title=\"single V Groove Butt Weld {}° groove angle\".format(alpha.value),\n", + " xlabel=\"millimeter\",\n", + " ylabel=\"millimeter\",\n", + ")\n", "\n", - "plt.plot(profile_data[0],profile_data[1],\".\")" + "plt.plot(profile_data[0], profile_data[1], \".\")" ] }, { @@ -394,7 +409,7 @@ "source": [ "# Values for the Single V Groove Butt Weld\n", "# in a dictionary\n", - "t = Quantity(0.009,unit=\"meter\")\n", + "t = Quantity(0.009, unit=\"meter\")\n", "alpha = Quantity(60, unit=\"deg\")\n", "b = Quantity(0.2, unit=\"centimeter\")\n", "c = Quantity(1, unit=\"millimeter\")\n", @@ -444,12 +459,16 @@ "ax = plt.gca()\n", "ax.cla()\n", "ax.axis(\"equal\")\n", - "#ax.axis([-7, +7, -1, 20])\n", - "#[xmin, xmax, ymin, ymax]\n", + "# ax.axis([-7, +7, -1, 20])\n", + "# [xmin, xmax, ymin, ymax]\n", "ax.grid(True)\n", - "ax.set(title=\"single V Groove Butt Weld {}° groove angle\".format(alpha.value), xlabel=\"millimeter\", ylabel=\"millimeter\")\n", + "ax.set(\n", + " title=\"single V Groove Butt Weld {}° groove angle\".format(alpha.value),\n", + " xlabel=\"millimeter\",\n", + " ylabel=\"millimeter\",\n", + ")\n", "\n", - "plt.plot(profile_data[0],profile_data[1],\".\")" + "plt.plot(profile_data[0], profile_data[1], \".\")" ] }, { @@ -509,21 +528,18 @@ "ax = plt.gca()\n", "ax.cla()\n", "ax.axis(\"equal\")\n", - "#ax.axis([-7, +7, -1, 20])\n", - "#[xmin, xmax, ymin, ymax]\n", + "# ax.axis([-7, +7, -1, 20])\n", + "# [xmin, xmax, ymin, ymax]\n", "ax.grid(True)\n", - "ax.set(title=\"single U Groove Butt Weld {}° groove angle\".format(beta.value), xlabel=\"millimeter\", ylabel=\"millimeter\")\n", + "ax.set(\n", + " title=\"single U Groove Butt Weld {}° groove angle\".format(beta.value),\n", + " xlabel=\"millimeter\",\n", + " ylabel=\"millimeter\",\n", + ")\n", "\n", - "plt.plot(profile_data[0],profile_data[1],\".\")" + "plt.plot(profile_data[0], profile_data[1], \".\")" ] }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - }, { "cell_type": "code", "execution_count": null, From 3c0c577f3b5bb699ffc0135003fb0f3a55113426 Mon Sep 17 00:00:00 2001 From: mnagel Date: Wed, 12 Feb 2020 15:25:19 +0100 Subject: [PATCH 174/177] imperative mood is hard!!! --- mypackage/all_groove.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mypackage/all_groove.py b/mypackage/all_groove.py index 6d55603..a7167c6 100644 --- a/mypackage/all_groove.py +++ b/mypackage/all_groove.py @@ -10,7 +10,7 @@ def singleVGrooveButtWeld( t, alpha, b, c, width_default=Quantity(5, unit="millimeter") ): """ - Calculates a Single-V Groove Butt Weld. + Calculate a Single-V Groove Butt Weld. :param t: the workpiece thickness, as Astropy unit :param alpha: the groove angle, as Astropy unit @@ -56,7 +56,7 @@ def singleUGrooveButtWeld( t, beta, R, b, c, width_default=Quantity(15, unit="millimeter") ): """ - Calculates a Single-U Groove Butt Weld. + Calculate a Single-U Groove Butt Weld. :param t: the workpiece thickness, as Astropy unit :param beta: the bevel angle, as Astropy unit From ef7cbfd550214f83c6361d30fe2a1d7a95a78e07 Mon Sep 17 00:00:00 2001 From: mnagel Date: Wed, 19 Feb 2020 13:03:17 +0100 Subject: [PATCH 175/177] fix for edges. Depending on input width, the edge will be calculated --- mypackage/all_groove.py | 39 ++++++++++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/mypackage/all_groove.py b/mypackage/all_groove.py index a7167c6..bf0b8d6 100644 --- a/mypackage/all_groove.py +++ b/mypackage/all_groove.py @@ -7,7 +7,7 @@ def singleVGrooveButtWeld( - t, alpha, b, c, width_default=Quantity(5, unit="millimeter") + t, alpha, b, c, width_default=Quantity(2, unit="millimeter") ): """ Calculate a Single-V Groove Butt Weld. @@ -17,7 +17,7 @@ def singleVGrooveButtWeld( :param b: the root opening, as Astropy unit :param c: the root face, as Astropy unit :param width_default: the width of the workpiece, as Astropy unit - :return: point_could_generator.Profile + :return: geo.Profile """ t = t.to_value("millimeter") alpha = alpha.to_value("rad") @@ -25,6 +25,15 @@ def singleVGrooveButtWeld( c = c.to_value("millimeter") width = width_default.to_value("millimeter") + # calculations: + s = np.tan(alpha / 2) * (t - c) + + # Rand breiten Berechnung + edge = np.min([-s, 0]) + if width <= -edge + 1: + # zu Kleine Breite für die Naht wird angepasst + width = width - edge + segment_list = [] bottom = geo.LineSegment([[-width, 0], [0, 0]]) @@ -34,7 +43,6 @@ def singleVGrooveButtWeld( root_face = geo.LineSegment([[0, 0], [0, c]]) segment_list.append(root_face) - s = np.tan(alpha / 2) * (t - c) groove_face = geo.LineSegment([[0, -s], [c, t]]) segment_list.append(groove_face) @@ -53,7 +61,7 @@ def singleVGrooveButtWeld( def singleUGrooveButtWeld( - t, beta, R, b, c, width_default=Quantity(15, unit="millimeter") + t, beta, R, b, c, width_default=Quantity(3, unit="millimeter") ): """ Calculate a Single-U Groove Butt Weld. @@ -64,7 +72,7 @@ def singleUGrooveButtWeld( :param b: the root opening, as Astropy unit :param c: the root face, as Astropy unit :param width_default: the width of the workpiece, as Astropy unit - :return: point_could_generator.Profile + :return: geo.Profile """ t = t.to_value("millimeter") beta = beta.to_value("rad") @@ -73,6 +81,21 @@ def singleUGrooveButtWeld( c = c.to_value("millimeter") width = width_default.to_value("millimeter") + # calculations: + # vom nächsten Punkt zum Kreismittelpunkt ist der Vektor (x,y) + x = R * np.cos(beta) + y = R * np.sin(beta) + # m = [0,c+R] Kreismittelpunkt + # => [-x,c+R-y] ist der nächste Punkt + + s = np.tan(beta) * (t - (c + R - y)) + + # Rand breiten Berechnung + edge = np.min([-x - s, 0]) + if width <= -edge + 1: + # zu Kleine Breite für die Naht wird angepasst + width = width - edge + segment_list = [] bottom = geo.LineSegment([[-width, 0], [0, 0]]) @@ -82,16 +105,10 @@ def singleUGrooveButtWeld( root_face = geo.LineSegment([[0, 0], [0, c]]) segment_list.append(root_face) - # vom nächsten Punkt zum Kreismittelpunkt ist der Vektor (x,y) - x = R * np.cos(beta) - y = R * np.sin(beta) - # m = [0,c+R] Kreismittelpunkt - # => [-x,c+R-y] ist der nächste Punkt groove_face_arc = geo.ArcSegment([[0, -x, 0], [c, c + R - y, c + R]], False) segment_list.append(groove_face_arc) - s = np.tan(beta) * (t - (c + R - y)) groove_face_line = geo.LineSegment([[-x, -x - s], [c + R - y, t]]) segment_list.append(groove_face_line) From b7b6e64a07221b376d966913c77789e0b5f7a911 Mon Sep 17 00:00:00 2001 From: mnagel Date: Mon, 24 Feb 2020 08:40:04 +0100 Subject: [PATCH 176/177] Added pytest for the u and V groove. The pynb is now an instruction. Added a rough structure for calling different groove types. --- Weld_tester.ipynb | 426 ++++++---------------------------------- mypackage/all_groove.py | 14 ++ tests/test_groove.py | 37 ++++ 3 files changed, 107 insertions(+), 370 deletions(-) create mode 100644 tests/test_groove.py diff --git a/Weld_tester.ipynb b/Weld_tester.ipynb index 2b65d75..506db6c 100644 --- a/Weld_tester.ipynb +++ b/Weld_tester.ipynb @@ -7,82 +7,24 @@ "outputs": [], "source": [ "import matplotlib.pyplot as plt\n", - "from astropy.units import Quantity\n", - "import numpy as np\n", - "import copy\n", - "\n", - "import mypackage.geometry as geo\n", - "import mypackage.transformations as tr" + "from mypackage.all_groove import grooveType\n", + "from astropy.units import Quantity" ] }, { - "cell_type": "code", - "execution_count": 2, + "cell_type": "markdown", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[[2. 1.89473684 1.78947368 1.68421053 1.57894737 1.47368421\n", - " 1.36842105 1.26315789 1.15789474 1.05263158 0.94736842 0.84210526\n", - " 0.73684211 0.63157895 0.52631579 0.42105263 0.31578947 0.21052632\n", - " 0.10526316 0. ]\n", - " [3. 2.89473684 2.78947368 2.68421053 2.57894737 2.47368421\n", - " 2.36842105 2.26315789 2.15789474 2.05263158 1.94736842 1.84210526\n", - " 1.73684211 1.63157895 1.52631579 1.42105263 1.31578947 1.21052632\n", - " 1.10526316 1. ]]\n" - ] - }, - { - "data": { - "text/plain": [ - "[]" - ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], "source": [ - "seg = geo.LineSegment([[2, 0], [3, 1]])\n", - "seg_data = seg.rasterize(0.15)\n", - "print(seg_data)\n", - "plt.plot(seg_data[0], seg_data[1])" + "# Loading from dictionary\n", + "\n", + "Construct a dictionary with astropy Quantity. The Quantity must have the corresponding units, and the function converts the corresponding units into millimeters or rad." ] }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 2, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'t': ,\n", - " 'alpha': ,\n", - " 'b': ,\n", - " 'c': }" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# Values for the Single V Groove Butt Weld\n", "# in a dictionary\n", @@ -90,209 +32,50 @@ "alpha = Quantity(40, unit=\"deg\")\n", "b = Quantity(0.2, unit=\"centimeter\")\n", "c = Quantity(1, unit=\"millimeter\")\n", - "v_naht_dict = dict(t=t, alpha=alpha, b=b, c=c)\n", - "width_default = Quantity(5, unit=\"millimeter\")\n", - "v_naht_dict" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [], - "source": [ - "# V-naht berechnungen und erstellen der einzelnen Teilsegmente\n", - "bottom = geo.LineSegment([[-width_default.to_value(\"millimeter\"), 0], [0, 0]])\n", - "root_face = geo.LineSegment([[0, 0], [0, v_naht_dict[\"b\"].to_value(\"millimeter\")]])\n", - "alpha_halbe = v_naht_dict[\"alpha\"].to_value(\"deg\") / 2\n", - "s = np.tan(alpha_halbe)\n", - "groove_face = geo.LineSegment(\n", - " [\n", - " [0, -s],\n", - " [\n", - " v_naht_dict[\"b\"].to_value(\"millimeter\"),\n", - " v_naht_dict[\"t\"].to_value(\"millimeter\"),\n", - " ],\n", - " ]\n", - ")\n", - "top = geo.LineSegment(\n", - " [\n", - " [-s, -width_default.to_value(\"millimeter\")],\n", - " [\n", - " v_naht_dict[\"t\"].to_value(\"millimeter\"),\n", - " v_naht_dict[\"t\"].to_value(\"millimeter\"),\n", - " ],\n", - " ]\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[]" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAWoAAAD4CAYAAADFAawfAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4xLjEsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy8QZhcZAAASP0lEQVR4nO3de4yddZ3H8fe3Hcq1CKQ1aAu2gixUVsQMLLdFuUhrgbbzJLthEwzuJkvWuIrG1RVNdrP/bdS4bvaWEGETI9GohVJapMhF5Q4FilIKishVLgPIbbmUtt/945lBGKad03ae83vOnPcrITNzzunMJ/TMp8/5nt/z/CIzkSS117TSASRJ22ZRS1LLWdSS1HIWtSS1nEUtSS030MQ3nTVrVs6bN6+Jby1JU9Idd9zxTGbOHu++Rop63rx5rF27tolvLUlTUkQ8vLX7HH1IUstZ1JLUcha1JLWcRS1JLWdRS1LLWdSS1HIWtSS1XCPrqNXDvv992LChdIrumjYNPvUp8CQttZRFrbf70Y9gxYrSKborEx55BC66qHQSaVwWtd7ukktKJ+i+s8+GlSth0yYY8FdC7eOMWqoqePZZuP760kmkcVnU0sKFsPvu/flqQj3Bopb23LMu60svhS1bSqeR3sGilqAefzz+OHjVR7WQRS0BnHFG/Uai4w+1kEUtAey7L5x8MixfXi/Xk1rEopZGVRU88ACsX186ifQ2FrU0aulSiHD8odaxqKVR++8Pxx1nUat1LGrpraoK7r4bHnywdBLpTRa19FZDQ/XHSy8tm0N6C4taeqv58+HIIx1/qFUsammsqoKbboInniidRAIsaumdHH+oZSxqaawFC+CQQyxqtYZFLY0VUY8/rrsOnnuudBrJopbGVVWweTOsWlU6idRZUUfEFyJifUTcExHfj4jdmg4mFTU4CHPnuvpDrTBhUUfEHOBzwGBmHg5MB85qOphU1Oj4Y80aePnl0mnU5zodfQwAu0fEALAH8PvmIkktMTQEr70GV15ZOon63IRFnZmPA98EHgGeAF7IzKvGPi4izo2ItRGxdnh4ePKTSt12wgkwa5arP1RcJ6OPfYGlwHzgvcCeEXH22Mdl5gWZOZiZg7Nnz578pFK3DQzUV9RbtQpef710GvWxTkYfpwK/y8zhzHwDuAQ4rtlYUktUFbz4Ilx7bekk6mOdFPUjwDERsUdEBHAKsKHZWFJLnHIKzJzp6g8V1cmM+lbgx8CdwK9G/swFDeeS2mHXXev9FC+7rF5XLRXQ0aqPzPznzDw0Mw/PzE9mpgM79Y+qguFhuPHG0knUpzwzUZrIokX1kbXjDxViUUsT2WsvWLiwLmp3KFcBFrXUiaqCRx+FO+4onUR9yKKWOnHmmTB9uuMPFWFRS53Ybz846STPUlQRFrXUqaqC++6De+8tnUR9xqKWOrVsWX1VPY+q1WUWtdSp97wHjjnGObW6zqKWtkdVwZ13wkMPlU6iPmJRS9tjdIfyFSvK5lBfsail7XHQQXDEEY4/1FUWtbS9qgpuuAGeeqp0EvUJi1raXkND9ankl11WOon6hEUtba/DD4eDD3b8oa6xqKXtNbpD+bXXwvPPl06jPmBRSzuiquCNN2D16tJJ1AcsamlHHHUUzJnj+ENdYVFLO2LatPpNxZ/8BF55pXQaTXEWtbSjhobg1VdhzZrSSTTFWdTSjjrxxPryp44/1DCLWtpRAwOwdCmsWgUbN5ZOoynMopZ2RlXVS/R+9rPSSTSFWdTSzjj11HrzW8cfapBFLe2M3XaDxYvrq+lt3lw6jaYoi1raWVVVX6Dp5ptLJ9EUZVFLO2vxYpgxwy261BiLWtpZM2fCaafVc+rM0mk0BVnU0mSoqnp7rnXrSifRFGRRS5PhzDPr08pd/aEGWNTSZJg1Cz76UYtajbCopclSVXDvvXD//aWTaIqxqKXJsmxZ/dHVH5pkFrU0WebOhaOPdvyhSWdRS5OpquD22+GRR0on0RRiUUuTaWio/rhiRdkcmlI6KuqI2CcifhwR90XEhog4tulgUk865JB6l3LHH5pEnR5R/ztwZWYeChwBbGguktTjhobg+uvh6adLJ9EUMWFRR8TewInAhQCZuTEzn286mNSzqgq2bIGVK0sn0RTRyRH1+4Fh4H8j4q6I+E5E7Dn2QRFxbkSsjYi1w8PDkx5U6hlHHAHz57tMT5Omk6IeAD4C/E9mHgn8H/CVsQ/KzAsyczAzB2fPnj3JMaUeElEfVV99NbzwQuk0mgI6KerHgMcy89aRr39MXdyStmZoqN5HcfXq0kk0BUxY1Jn5JPBoRPzJyE2nAPc2mkrqdcceC/vv7/hDk2Kgw8d9Frg4ImYADwJ/3VwkaQqYNq0+pfy734VXX4Xddy+dSD2so+V5mbluZP78ocxclpl/aDqY1POqCl55Ba66qnQS9TjPTJSa8rGPwT77OP7QTrOopabssgssWVKvp37jjdJp1MMsaqlJVQV/+AP8/Oelk6iHWdRSk047DfbYw2t/aKdY1FKTdt8dPvGJek69ZUvpNOpRFrXUtKqCJ5+EW24pnUQ9yqKWmnb66fUbi67+0A6yqKWmvetdcOqp9Zw6s3Qa9SCLWuqGqoIHH4Rf/rJ0EvUgi1rqhiVL6tPKXf2hHWBRS93w7nfDCSdY1NohFrXULVUF99wDv/lN6STqMRa11C2jO5S7+kPbyaKWuuXAA2FwEJYvL51EPcailrqpquC22+Cxx0onUQ+xqKVuGh1/rFhRNod6ikUtddOhh8Jhh7n6Q9vFopa6rargF7+AZ54pnUQ9wqKWuq2qYPNmuPzy0knUIyxqqduOPBLe9z7HH+qYRS11W0T9puJVV8FLL5VOox5gUUslVBVs3AhXXFE6iXqARS2VcNxx9fU/PEtRHbCopRKmT4dly2D1anjttdJp1HIWtVTK0BC8/DJcfXXpJGo5i1oq5eSTYe+9Xf2hCVnUUikzZsCZZ8LKlbBpU+k0ajGLWiqpquDZZ+H660snUYtZ1FJJCxfCbrs5/tA2WdRSSXvuCYsW1cv0tmwpnUYtZVFLpVUVPP443H576SRqKYtaKu2MM2BgwJNftFUWtVTavvvWS/WWL4fM0mnUQha11AZVBQ88AOvXl06iFrKopTZYurS+qp6rPzSOjos6IqZHxF0RsarJQFJf2n//+kJNFrXGsT1H1OcBG5oKIvW9qoK774bf/rZ0ErVMR0UdEXOB04HvNBtH6mOjO5S7+kNjdHpE/W3gy8BWV+RHxLkRsTYi1g4PD09KOKmvzJ9fb9NlUWuMCYs6Is4Ans7MO7b1uMy8IDMHM3Nw9uzZkxZQ6itVBTfdBE88UTqJWqSTI+rjgSUR8RDwA+DkiPheo6mkfjU6/lixomwOtcqERZ2Z52fm3MycB5wFXJuZZzeeTOpHCxbAIYe4+kNv4zpqqU0i6vHHddfBc8+VTqOW2K6izsyfZeYZTYWRRF3UmzfD5ZeXTqKW8IhaapvBQZg719UfepNFLbXN6PhjzZp681v1PYtaaqOqgtdegyuvLJ1ELWBRS210wgkwa1Z96VP1PYtaaqPp0+sr6q1eDa+/XjqNCrOopbaqKnjpJbjmmtJJVJhFLbXVKafAzJmu/pBFLbXWrrvW+ymuWFGvq1bfsqilNqsqeOYZuOGG0klUkEUttdmiRfWRtdf+6GsWtdRme+0FCxfWc2p3KO9bFrXUdlUFjz4Kd2zzkvCawixqqe3OPLNeV+34o29Z1FLb7bcfnHRSfZai44++ZFFLvaCq4Ne/hg0bSidRARa11AuWLq0/Ov7oSxa11Ave+1449liLuk9Z1FKvqCq46y546KHSSdRlFrXUK0Z3KPfaH33HopZ6xUEHwRFHOP7oQxa11EuqCm68EZ58snQSdZFFLfWSoaF6LfVll5VOoi6yqKVecvjhcPDBzqn7jEUt9ZLRHcqvuQaef750GnWJRS31mqqCTZtg1arSSdQlFrXUa446CubMcfVHH7GopV4zbRosWwZXXgmvvFI6jbrAopZ6UVXBq6/CmjWlk6gLLGqpF514Yn35U8cffcGilnrRwEB9Rb3LL4eNG0unUcMsaqlXDQ3BCy/AddeVTqKGWdRSr/r4x2HPPR1/9AGLWupVu+0Gp59en06+eXPpNGqQRS31sqqCp56Cm28unUQNsqilXrZ4McyY4fhjipuwqCPigIi4LiI2RMT6iDivG8EkdWDmzHpWfckl7lA+hXVyRL0J+GJmHgYcA3wmIhY0G0tSx6oKHn4Y1q0rnUQNmbCoM/OJzLxz5POXgA3AnKaDSerQkiX1aeWOP6as7ZpRR8Q84Ejg1nHuOzci1kbE2uHh4clJJ2lis2bBgQfWR9Wakjou6ojYC1gOfD4zXxx7f2ZekJmDmTk4e/bsycwoaSIRpROoQR0VdUTsQl3SF2emr68kqYs6WfURwIXAhsz8VvORJElv1ckR9fHAJ4GTI2LdyH+LG84lSRoxMNEDMvMGwAGYJBXimYmS1HIWtSS1nEUtSS1nUUtSy1nUktRyFrUktZxFLUktZ1FLUstZ1JLUcha1JLWcRS1JLWdRS1LLWdSS1HIWtSS1nEUtSS1nUUtSy1nUktRyFrUktZxFLUktZ1FLUstZ1JLUcha1JLWcRS1JLWdRS1LLWdSS1HIWtSS1nEUtSS1nUUtSy1nUktRyFrUktZxFLUktZ1FLUstZ1JLUcha1JLWcRS1JLddRUUfEooi4PyIeiIivNB1KkvRHExZ1REwH/gv4BLAA+KuIWNB0MElSbaCDxxwNPJCZDwJExA+ApcC9TQZTIZ/7HFxzTekU2l6PPlo6gRrUSVHPAd76LHgM+LOxD4qIc4FzAQ488MBJCacCDjgAFviCqecsWADnnFM6hRrSSVHHOLflO27IvAC4AGBwcPAd96tHfOlLpRNIGqOTNxMfAw54y9dzgd83E0eSNFYnRX078IGImB8RM4CzgJXNxpIkjZpw9JGZmyLi74E1wHTgosxc33gySRLQ2YyazLwCuKLhLJKkcXhmoiS1nEUtSS1nUUtSy1nUktRykTn556ZExDDwMDALeGbSf0BzzNss8zbLvM1qOu/7MnP2eHc0UtRvfvOItZk52NgPmGTmbZZ5m2XeZpXM6+hDklrOopaklmu6qC9o+PtPNvM2y7zNMm+ziuVtdEYtSdp5jj4kqeUsaklquUaKOiL+IiLWR8SWiBgcc9/5I5vk3h8RC5v4+TsjIj4cEbdExLqIWBsRR5fONJGI+OzI/8/1EfH10nk6ERH/EBEZEbNKZ9mWiPhGRNwXEb+MiEsjYp/Smcbqpc2nI+KAiLguIjaMPF/PK52pExExPSLuiohVJX5+U0fU9wAV8Iu33jiyKe5ZwAeBRcB/j2ye2yZfB/4lMz8M/NPI160VESdR72H5ocz8IPDNwpEmFBEHAB8HHimdpQM/BQ7PzA8BvwbOL5znbXpw8+lNwBcz8zDgGOAzLc876jxgQ6kf3khRZ+aGzLx/nLuWAj/IzNcz83fAA9Sb57ZJAnuPfP4u2r+bzaeBf83M1wEy8+nCeTrxb8CXGWdLt7bJzKsyc9PIl7dQ73DUJm9uPp2ZG4HRzadbKTOfyMw7Rz5/ibr85pRNtW0RMRc4HfhOqQzdnlGPt1Fu2/6SPg98IyIepT46bdUR1DgOAf48Im6NiJ9HxFGlA21LRCwBHs/Mu0tn2QF/A/ykdIgxeuF3alwRMQ84Eri1bJIJfZv6wGJLqQAdbRwwnoi4Gth/nLu+lpmXbe2PjXNb14+qtpUdOAX4QmYuj4i/BC4ETu1mvrEmyDsA7Ev9MvIo4IcR8f4suO5ygrxfBU7rbqJt6+S5HBFfo37ZfnE3s3WgFb9T2ysi9gKWA5/PzBdL59maiDgDeDoz74iIj5XKscNFnZk7Ul6t2Ch3W9kj4rvU8yiAH1Hw5c6oCfJ+GrhkpJhvi4gt1BePGe5WvrG2ljci/hSYD9wdEVD//d8ZEUdn5pNdjPg2Ez2XI+Ic4AzglJL/AG5FK36ntkdE7EJd0hdn5iWl80zgeGBJRCwGdgP2jojvZebZ3QzR7dHHSuCsiNg1IuYDHwBu63KGifwe+OjI5ycDvymYpRMrqHMSEYcAM2jpFcky81eZ+e7MnJeZ86hL5iMlS3oiEbEI+EdgSWa+UjrPOHpq8+mo/4W+ENiQmd8qnWcimXl+Zs4deb6eBVzb7ZKGnTii3paIGAL+A5gNrI6IdZm5MDPXR8QPgXupX0Z+JjM3N5FhJ/wt8O8RMQC8BpxbOM9ELgIuioh7gI3AOS086utl/wnsCvx05FXALZn5d2Uj/VEPbj59PPBJ4FcRsW7ktq+O7MuqrfAUcklqOc9MlKSWs6glqeUsaklqOYtaklrOopaklrOoJanlLGpJarn/B1/ZqUoFFFbqAAAAAElFTkSuQmCC\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "# Segmente zusammenfassen zu shape\n", - "segment_list = []\n", - "segment_list.append(bottom)\n", - "segment_list.append(root_face)\n", - "segment_list.append(groove_face)\n", - "segment_list.append(top)\n", - "shape = geo.Shape(segment_list)\n", - "\n", - "shape_data = shape.rasterize(0.8)\n", - "\n", - "ax = plt.gca()\n", - "ax.cla()\n", - "ax.axis(\"equal\")\n", - "\n", - "plt.plot(shape_data[0], shape_data[1], \"r-\")" + "v_naht_dict = dict(t=t, alpha=alpha, b=b, c=c)" ] }, { - "cell_type": "code", - "execution_count": 6, + "cell_type": "markdown", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "1.0\n" - ] - }, - { - "data": { - "text/plain": [ - "[]" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], "source": [ - "# shape verschieben und verdoppeln\n", - "b_halbe = v_naht_dict[\"b\"].to_value(\"millimeter\") / 2\n", - "print(b_halbe)\n", - "shape = shape.translate([-b_halbe, 0])\n", - "shape_r = shape.reflect_across_line([0, 0], [0, 1])\n", - "\n", - "shape_r_data = shape_r.rasterize(0.1)\n", - "\n", - "ax = plt.gca()\n", - "ax.cla()\n", - "ax.axis(\"equal\")\n", - "\n", - "plt.plot(shape_r_data[0], shape_r_data[1], \"r-\")" + "Now insert the dictionary into the function:" ] }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 3, "metadata": {}, "outputs": [], "source": [ - "# profil erstellen\n", - "profile = geo.Profile([shape, shape_r])" + "profile01 = grooveType(v_naht_dict, groove_type=\"v\")" ] }, { - "cell_type": "code", - "execution_count": 11, + "cell_type": "markdown", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[]" - ] - }, - "execution_count": 11, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], "source": [ - "profile_data = profile.rasterize(0.3)\n", - "shape_data = shape.rasterize(0.3)\n", - "shape_r_data = shape_r.rasterize(0.3)\n", - "\n", - "ax = plt.gca()\n", - "ax.cla()\n", - "ax.axis(\"equal\")\n", - "\n", - "plt.plot(shape_data[0], shape_data[1], \"r.\")\n", - "plt.plot(shape_r_data[0], shape_r_data[1], \"r.\")\n", - "# plt.plot(profile_data[0],profile_data[1],\"--\")" + "Rasterizing and plotting:" ] }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 4, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "[]" + "[]" ] }, - "execution_count": 12, + "execution_count": 4, "metadata": {}, "output_type": "execute_result" }, { "data": { - "image/png": "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\n", + "image/png": "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\n", "text/plain": [ "
" ] @@ -304,56 +87,27 @@ } ], "source": [ - "h = len(profile_data[0]) // 2\n", + "profile01_data = profile01.rasterize(0.2)\n", "\n", "ax = plt.gca()\n", "ax.cla()\n", "ax.axis(\"equal\")\n", + "# ax.axis([-7, +7, -1, 20])\n", + "ax.grid(True)\n", + "ax.set(\n", + " title=f\"single V Groove Butt Weld {alpha.value}° groove angle\",\n", + " xlabel=\"millimeter\",\n", + " ylabel=\"millimeter\",\n", + ")\n", "\n", - "plt.plot(profile_data[0][:h], profile_data[1][:h], \".\")\n", - "plt.plot(profile_data[0][h:], profile_data[1][h:], \".\")" + "plt.plot(profile01_data[0], profile01_data[1], \".\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "# Loading from file" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "import matplotlib.pyplot as plt\n", - "from mypackage.all_groove import singleVGrooveButtWeld, singleUGrooveButtWeld\n", - "from astropy.units import Quantity" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "# Values for the Single V Groove Butt Weld\n", - "# in a dictionary\n", - "t = Quantity(0.009, unit=\"meter\")\n", - "alpha = Quantity(40, unit=\"deg\")\n", - "b = Quantity(0.2, unit=\"centimeter\")\n", - "c = Quantity(1, unit=\"millimeter\")\n", - "v_naht_dict = dict(t=t, alpha=alpha, b=b, c=c)" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [], - "source": [ - "profile = singleVGrooveButtWeld(**v_naht_dict)" + "Another example of a Single-V Groove Butt Weld with a 60° Groove Angle:" ] }, { @@ -364,7 +118,7 @@ { "data": { "text/plain": [ - "[]" + "[]" ] }, "execution_count": 5, @@ -373,7 +127,7 @@ }, { "data": { - "image/png": "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\n", + "image/png": "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\n", "text/plain": [ "
" ] @@ -384,28 +138,6 @@ "output_type": "display_data" } ], - "source": [ - "profile_data = profile.rasterize(0.2)\n", - "\n", - "ax = plt.gca()\n", - "ax.cla()\n", - "ax.axis(\"equal\")\n", - "# ax.axis([-7, +7, -1, 20])\n", - "ax.grid(True)\n", - "ax.set(\n", - " title=\"single V Groove Butt Weld {}° groove angle\".format(alpha.value),\n", - " xlabel=\"millimeter\",\n", - " ylabel=\"millimeter\",\n", - ")\n", - "\n", - "plt.plot(profile_data[0], profile_data[1], \".\")" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [], "source": [ "# Values for the Single V Groove Butt Weld\n", "# in a dictionary\n", @@ -413,48 +145,11 @@ "alpha = Quantity(60, unit=\"deg\")\n", "b = Quantity(0.2, unit=\"centimeter\")\n", "c = Quantity(1, unit=\"millimeter\")\n", - "v_naht_dict = dict(t=t, alpha=alpha, b=b, c=c)" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [], - "source": [ - "profile = singleVGrooveButtWeld(**v_naht_dict)" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[]" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "profile_data = profile.rasterize(0.2)\n", + "v_naht_dict = dict(t=t, alpha=alpha, b=b, c=c)\n", + "\n", + "profile02 = grooveType(v_naht_dict, groove_type=\"v\")\n", + "\n", + "profile02_data = profile02.rasterize(0.2)\n", "\n", "ax = plt.gca()\n", "ax.cla()\n", @@ -463,55 +158,39 @@ "# [xmin, xmax, ymin, ymax]\n", "ax.grid(True)\n", "ax.set(\n", - " title=\"single V Groove Butt Weld {}° groove angle\".format(alpha.value),\n", + " title=f\"single V Groove Butt Weld {alpha.value}° groove angle\",\n", " xlabel=\"millimeter\",\n", " ylabel=\"millimeter\",\n", ")\n", "\n", - "plt.plot(profile_data[0], profile_data[1], \".\")" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [], - "source": [ - "t = Quantity(15, unit=\"millimeter\")\n", - "beta = Quantity(9, unit=\"deg\")\n", - "R = Quantity(6, unit=\"millimeter\")\n", - "b = Quantity(3, unit=\"millimeter\")\n", - "c = Quantity(1, unit=\"millimeter\")\n", - "u_naht_dict = dict(t=t, beta=beta, R=R, b=b, c=c)" + "plt.plot(profile02_data[0], profile02_data[1], \".\")" ] }, { - "cell_type": "code", - "execution_count": 8, + "cell_type": "markdown", "metadata": {}, - "outputs": [], "source": [ - "profile = singleUGrooveButtWeld(**u_naht_dict)" + "An example of a Single-U Groove Butt Weld:" ] }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 6, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "[]" + "[]" ] }, - "execution_count": 12, + "execution_count": 6, "metadata": {}, "output_type": "execute_result" }, { "data": { - "image/png": "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\n", + "image/png": "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\n", "text/plain": [ "
" ] @@ -523,21 +202,28 @@ } ], "source": [ - "profile_data = profile.rasterize(0.5)\n", + "t = Quantity(15, unit=\"millimeter\")\n", + "beta = Quantity(9, unit=\"deg\")\n", + "R = Quantity(6, unit=\"millimeter\")\n", + "b = Quantity(3, unit=\"millimeter\")\n", + "c = Quantity(1, unit=\"millimeter\")\n", + "u_naht_dict = dict(t=t, beta=beta, R=R, b=b, c=c)\n", + "\n", + "profile03 = grooveType(u_naht_dict, groove_type=\"u\")\n", + "\n", + "profile03_data = profile03.rasterize(0.5)\n", "\n", "ax = plt.gca()\n", "ax.cla()\n", "ax.axis(\"equal\")\n", - "# ax.axis([-7, +7, -1, 20])\n", - "# [xmin, xmax, ymin, ymax]\n", "ax.grid(True)\n", "ax.set(\n", - " title=\"single U Groove Butt Weld {}° groove angle\".format(beta.value),\n", + " title=f\"single U Groove Butt Weld {beta.value}° groove angle\",\n", " xlabel=\"millimeter\",\n", " ylabel=\"millimeter\",\n", ")\n", "\n", - "plt.plot(profile_data[0], profile_data[1], \".\")" + "plt.plot(profile03_data[0], profile03_data[1], \".\")" ] }, { diff --git a/mypackage/all_groove.py b/mypackage/all_groove.py index bf0b8d6..035d6ff 100644 --- a/mypackage/all_groove.py +++ b/mypackage/all_groove.py @@ -6,6 +6,20 @@ import mypackage.geometry as geo +def grooveType(dictionary, groove_type): + """ + Calculate a Groove type with given dictionary and goove type. + + :param dictionary: dictionary with the needed groove parameters + :param groove_type: string, string corresponding to the groove type. + """ + if groove_type == "v": + return singleVGrooveButtWeld(**dictionary) + + if groove_type == "u": + return singleUGrooveButtWeld(**dictionary) + + def singleVGrooveButtWeld( t, alpha, b, c, width_default=Quantity(2, unit="millimeter") ): diff --git a/tests/test_groove.py b/tests/test_groove.py new file mode 100644 index 0000000..1ba06fe --- /dev/null +++ b/tests/test_groove.py @@ -0,0 +1,37 @@ +from mypackage.all_groove import singleVGrooveButtWeld, singleUGrooveButtWeld + +from astropy.units import Quantity +from math import isclose + + +def test_v_groove(): + t = Quantity(6, unit="millimeter") + alpha = Quantity(73.73979529168803, unit="deg") + b = Quantity(2, unit="millimeter") + c = Quantity(2, unit="millimeter") + width = Quantity(6, unit="millimeter") + v_naht_dict = dict(t=t, alpha=alpha, b=b, c=c, width_default=width) + profile = singleVGrooveButtWeld(**v_naht_dict) + data = profile.rasterize(10) + test_data = [[-7, -1, -1, -4, -7, 7, 1, 1, 4, 7], + [0, 0, 2, 6, 6, 0, 0, 2, 6, 6]] + for i in range((len(test_data[0]))): + assert isclose(data[0][i], test_data[0][i]) + assert isclose(data[1][i], test_data[1][i]) + +def test_u_groove(): + t = Quantity(7, unit="millimeter") + beta = Quantity(9, unit="deg") + R = Quantity(2, unit="millimeter") + b = Quantity(2, unit="millimeter") + c = Quantity(2, unit="millimeter") + width = Quantity(6, unit="millimeter") + u_naht_dict = dict(t=t, beta=beta, R=R, b=b, c=c, width_default=width) + profile = singleUGrooveButtWeld(**u_naht_dict) + data = profile.rasterize(10) + test_data = [[-7, -1, -1, -2.97537668, -3.50008357, -7, 7, 1, 1, + 2.97537668, 3.50008357, 7], + [0, 0, 2, 3.68713107, 7, 7, 0, 0, 2, 3.68713107, 7, 7]] + for i in range((len(test_data[0]))): + assert isclose(data[0][i], test_data[0][i]) + assert isclose(data[1][i], test_data[1][i]) \ No newline at end of file From f464b5ae29bee0e53140be01d93bd07d274fcee6 Mon Sep 17 00:00:00 2001 From: mnagel Date: Mon, 24 Feb 2020 15:19:13 +0100 Subject: [PATCH 177/177] helper function added * groove function calls a helper function. * main function grooveType now calls the corresponding groove function --- Weld_tester.ipynb | 6 +- mypackage/all_groove.py | 139 +++++++++++++++++++++++++++++++--------- 2 files changed, 110 insertions(+), 35 deletions(-) diff --git a/Weld_tester.ipynb b/Weld_tester.ipynb index 506db6c..a849287 100644 --- a/Weld_tester.ipynb +++ b/Weld_tester.ipynb @@ -66,7 +66,7 @@ { "data": { "text/plain": [ - "[]" + "[]" ] }, "execution_count": 4, @@ -118,7 +118,7 @@ { "data": { "text/plain": [ - "[]" + "[]" ] }, "execution_count": 5, @@ -181,7 +181,7 @@ { "data": { "text/plain": [ - "[]" + "[]" ] }, "execution_count": 6, diff --git a/mypackage/all_groove.py b/mypackage/all_groove.py index 035d6ff..fccdce9 100644 --- a/mypackage/all_groove.py +++ b/mypackage/all_groove.py @@ -8,7 +8,7 @@ def grooveType(dictionary, groove_type): """ - Calculate a Groove type with given dictionary and goove type. + Calculate a Groove type. :param dictionary: dictionary with the needed groove parameters :param groove_type: string, string corresponding to the groove type. @@ -20,9 +20,8 @@ def grooveType(dictionary, groove_type): return singleUGrooveButtWeld(**dictionary) -def singleVGrooveButtWeld( - t, alpha, b, c, width_default=Quantity(2, unit="millimeter") -): +def singleVGrooveButtWeld(t, alpha, b, c, + width_default=Quantity(2, unit="millimeter")): """ Calculate a Single-V Groove Butt Weld. @@ -42,36 +41,48 @@ def singleVGrooveButtWeld( # calculations: s = np.tan(alpha / 2) * (t - c) - # Rand breiten Berechnung + # Rand breite edge = np.min([-s, 0]) if width <= -edge + 1: # zu Kleine Breite für die Naht wird angepasst width = width - edge + # x-values + x_value = [] + # y-values + y_value = [] segment_list = [] - bottom = geo.LineSegment([[-width, 0], [0, 0]]) - segment_list.append(bottom) + # bottom segment + x_value.append(-width) + y_value.append(0) + x_value.append(0) + y_value.append(0) + segment_list.append("line") + # root face if c != 0: - root_face = geo.LineSegment([[0, 0], [0, c]]) - segment_list.append(root_face) + x_value.append(0) + y_value.append(c) + segment_list.append("line") - groove_face = geo.LineSegment([[0, -s], [c, t]]) - segment_list.append(groove_face) + # groove face + x_value.append(-s) + y_value.append(t) + segment_list.append("line") - top = geo.LineSegment([[-s, -width], [t, t]]) - segment_list.append(top) + # top segment + x_value.append(-width) + y_value.append(t) + segment_list.append("line") - shape = geo.Shape(segment_list) + shape = _helperfunction(segment_list, [x_value, y_value]) shape = shape.translate([-b / 2, 0]) # y Achse als Spiegelachse shape_r = shape.reflect_across_line([0, 0], [0, 1]) - profile = geo.Profile([shape, shape_r]) - - return profile + return geo.Profile([shape, shape_r]) def singleUGrooveButtWeld( @@ -104,37 +115,101 @@ def singleUGrooveButtWeld( s = np.tan(beta) * (t - (c + R - y)) - # Rand breiten Berechnung + # Rand breite edge = np.min([-x - s, 0]) if width <= -edge + 1: # zu Kleine Breite für die Naht wird angepasst width = width - edge + # x-values + x_value = [] + # y-values + y_value = [] segment_list = [] - bottom = geo.LineSegment([[-width, 0], [0, 0]]) - segment_list.append(bottom) + # bottom segment + x_value.append(-width) + y_value.append(0) + x_value.append(0) + y_value.append(0) + segment_list.append("line") + # root face if c != 0: - root_face = geo.LineSegment([[0, 0], [0, c]]) - segment_list.append(root_face) + x_value.append(0) + y_value.append(c) + segment_list.append("line") - groove_face_arc = geo.ArcSegment([[0, -x, 0], [c, c + R - y, c + R]], - False) - segment_list.append(groove_face_arc) + # groove face arc kreismittelpunkt + x_value.append(0) + y_value.append(c + R) - groove_face_line = geo.LineSegment([[-x, -x - s], [c + R - y, t]]) - segment_list.append(groove_face_line) + # groove face arc + x_value.append(-x) + y_value.append(c + R - y) + segment_list.append("arc") - top = geo.LineSegment([[-x - s, -width], [t, t]]) - segment_list.append(top) + # groove face line + x_value.append(-x - s) + y_value.append(t) + segment_list.append("line") - shape = geo.Shape(segment_list) + # top segment + x_value.append(-width) + y_value.append(t) + segment_list.append("line") + + shape = _helperfunction(segment_list, [x_value, y_value]) shape = shape.translate([-b / 2, 0]) # y Achse als Spiegelachse shape_r = shape.reflect_across_line([0, 0], [0, 1]) - profile = geo.Profile([shape, shape_r]) + return geo.Profile([shape, shape_r]) + - return profile +def _helperfunction(liste, array): + """ + Calculate a shape from input. + Input liste der aufeinanderfolgenden Segmente als strings. + Input array der Punkte ich richtiger Reichenfolge. BSP: + array = [[x-werte], [y-werte]] + + :param liste: list of String, segment names ("line", "arc") + :param array: array of 2 array, + first array are x-values + second array are y-values + :return: geo.Shape + """ + segment_list = [] + counter = 0 + for elem in liste: + if elem == "line": + seg = geo.LineSegment( + [array[0][counter: counter + 2], + array[1][counter: counter + 2]] + ) + segment_list.append(seg) + counter += 1 + if elem == "arc": + arr0 = [ + # anfang + array[0][counter], + # ende + array[0][counter + 2], + # mittelpunkt + array[0][counter + 1], + ] + arr1 = [ + # anfang + array[1][counter], + # ende + array[1][counter + 2], + # mittelpunkt + array[1][counter + 1], + ] + seg = geo.ArcSegment([arr0, arr1], False) + segment_list.append(seg) + counter += 2 + + return geo.Shape(segment_list)