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/**' diff --git a/.codecov.yml b/.codecov.yml deleted file mode 100644 index be5f693..0000000 --- a/.codecov.yml +++ /dev/null @@ -1,35 +0,0 @@ -codecov: - branch: master - notify: - require_ci_to_pass: yes - -coverage: - precision: 2 - round: down - range: "70...100" - - status: - project: - default: - enabled: yes - target: 85% - threshold: 5% - - - patch: - default: - enabled: yes - target: 85% - -ignore: - - "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. 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 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 diff --git a/Weld_tester.ipynb b/Weld_tester.ipynb new file mode 100644 index 0000000..a849287 --- /dev/null +++ b/Weld_tester.ipynb @@ -0,0 +1,258 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import matplotlib.pyplot as plt\n", + "from mypackage.all_groove import grooveType\n", + "from astropy.units import Quantity" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 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": 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": "markdown", + "metadata": {}, + "source": [ + "Now insert the dictionary into the function:" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "profile01 = grooveType(v_naht_dict, groove_type=\"v\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Rasterizing and plotting:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[]" + ] + }, + "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": [ + "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(profile01_data[0], profile01_data[1], \".\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Another example of a Single-V Groove Butt Weld with a 60° Groove Angle:" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[]" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "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)\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", + "ax.axis(\"equal\")\n", + "# ax.axis([-7, +7, -1, 20])\n", + "# [xmin, xmax, ymin, ymax]\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(profile02_data[0], profile02_data[1], \".\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "An example of a Single-U Groove Butt Weld:" + ] + }, + { + "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": [ + "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.grid(True)\n", + "ax.set(\n", + " title=f\"single U Groove Butt Weld {beta.value}° groove angle\",\n", + " xlabel=\"millimeter\",\n", + " ylabel=\"millimeter\",\n", + ")\n", + "\n", + "plt.plot(profile03_data[0], profile03_data[1], \".\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "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/codecov.yml b/codecov.yml new file mode 100644 index 0000000..c371f9b --- /dev/null +++ b/codecov.yml @@ -0,0 +1,36 @@ +codecov: + branch: master + notify: + require_ci_to_pass: yes + +coverage: + precision: 2 + round: down + range: "70...100" + + status: + project: + default: + enabled: yes + target: 85% + threshold: 5% + + + patch: + default: + enabled: yes + target: 85% + threshold: 5% + +ignore: + - "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: 1 #e.g., 5. The number of uploaded reports codecov will receive before posting a comment on a pull request. 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/_utility.py b/mypackage/_utility.py new file mode 100644 index 0000000..a7dd591 --- /dev/null +++ b/mypackage/_utility.py @@ -0,0 +1,75 @@ +"""Contains package internal utility functions.""" + +import math +import numpy as np + + +def is_column_in_matrix(column, matrix): + """ + Check if a column (1d array) can be found inside of a matrix. + + :param column: Column that should be checked + :param matrix: Matrix + :return: True or False + """ + return is_row_in_matrix(column, np.transpose(matrix)) + + +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 matrix: Matrix + :return: True or False + """ + if not matrix.shape[1] == np.array(row).size: + return False + 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. + + 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 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/all_groove.py b/mypackage/all_groove.py new file mode 100644 index 0000000..fccdce9 --- /dev/null +++ b/mypackage/all_groove.py @@ -0,0 +1,215 @@ +"""provides the calculation of all Groove-Types.""" + +from astropy.units import Quantity +import numpy as np + +import mypackage.geometry as geo + + +def grooveType(dictionary, groove_type): + """ + Calculate a Groove 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")): + """ + Calculate 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: geo.Profile + """ + 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") + + # calculations: + s = np.tan(alpha / 2) * (t - c) + + # 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 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: + x_value.append(0) + y_value.append(c) + segment_list.append("line") + + # groove face + x_value.append(-s) + y_value.append(t) + segment_list.append("line") + + # 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]) + + return geo.Profile([shape, shape_r]) + + +def singleUGrooveButtWeld( + t, beta, R, b, c, width_default=Quantity(3, unit="millimeter") +): + """ + Calculate 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: geo.Profile + """ + 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") + + # 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 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 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: + x_value.append(0) + y_value.append(c) + segment_list.append("line") + + # groove face arc kreismittelpunkt + x_value.append(0) + y_value.append(c + R) + + # groove face arc + x_value.append(-x) + y_value.append(c + R - y) + segment_list.append("arc") + + # groove face line + x_value.append(-x - s) + y_value.append(t) + segment_list.append("line") + + # 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]) + + return geo.Profile([shape, shape_r]) + + +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) diff --git a/mypackage/geometry.py b/mypackage/geometry.py index cc6b29c..0a4fcc5 100644 --- a/mypackage/geometry.py +++ b/mypackage/geometry.py @@ -1,202 +1,1441 @@ """Provides classes to define lines and surfaces.""" +import mypackage._utility as ut +import mypackage.transformations as tf + +import copy +import math import numpy as np -def check_point_data_valid(point): - """ - Check if the data of a point is valid. +# LineSegment ----------------------------------------------------------------- - :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.") +class LineSegment: + """Line segment.""" + def __init__(self, points): + """ + Constructor. -# 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 points: 2x2 matrix of points. The first column is the + starting point and the second column the end point. + """ + 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): + raise ValueError("'points' is not a 2x2 matrix.") - :param row: Row that should be checked - :param array: 2d array - :return: True or False - """ - return (array == row).all(axis=1).any() + self._points = points + self._calculate_length() + def _calculate_length(self): + """ + Calculate the segment length from its points. -class Shape2D: - """Defines a shape in 2 dimensions.""" + :return: --- + """ + self._length = np.linalg.norm(self._points[:, 1] - self._points[:, 0]) + if math.isclose(self._length, 0): + raise ValueError("Segment length is 0.") + + @classmethod + def construct_with_points(cls, point_start, point_end): + """ + Construct a line segment with 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, segment_a, segment_b, weight): + """ + Interpolate two line segments linearly. + + :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(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) * segment_a.points + weight * segment_b.points + return cls(points) + + @property + def length(self): + """ + Get the segment length. + + :return: Segment length + """ + return self._length + + @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] - # Member variables -------------------------------------------------------- + @property + def points(self): + """ + Get the segments points in form of a 2x2 matrix. - min_segment_length = 1E-6 - tolerance_comparison = 1E-6 + The first column represents the starting point and the second one + the end point. - # Member classes ---------------------------------------------------------- + :return: 2x2 matrix containing the segments points + """ + return self._points - class Segment: - """Base class for segments.""" + 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 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 check_valid(self, point_start, point_end): - """ - Check if the segments data is valid. + def rasterize(self, raster_width): + """ + Create an array of points that describe the segments contour. - Checks if the segments data is compatible with the passed start - and end points. Raises an Exception if not. + The effective raster width may vary from the specified one, + since the algorithm enforces constant distances between two + raster points. - :param point_start: Starting point of the segment - :param point_end: End point of the segment - :return: --- - """ + :param raster_width: The desired distance between two raster points + :return: Array of contour points + """ + if not raster_width > 0: + raise ValueError("'raster_width' must be > 0") + raster_width = np.clip(np.abs(raster_width), None, self.length) - class LineSegment(Segment): - """Line segment.""" + num_raster_segments = np.round(self.length / raster_width) - class ArcSegment(Segment): - """Segment of a circle.""" + # normalized effective raster width + nerw = 1. / num_raster_segments - def __init__(self, point_center): - """ - Constructor. + multiplier = np.arange(0, 1 + 0.5 * nerw, nerw) + weight_matrix = np.array([1 - multiplier, multiplier]) - :param point_center: Center point of the arc - """ - point_center = np.array(point_center) - check_point_data_valid(point_center) + return np.matmul(self._points, weight_matrix) - self._point_center = point_center + def transform(self, matrix): + """ + Get a transformed copy of the segment. - def check_valid(self, point_start, point_end): - """ - Check if the segments data is valid. + :param matrix: Transformation matrix + :return: Transformed copy + """ + new_segment = copy.deepcopy(self) + new_segment.apply_transformation(matrix) + return new_segment - Checks if the segments data is compatible with the passed start - and end points. Raises an Exception if not. + def translate(self, vector): + """ + Get a translated copy of the segment. - :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 + :param vector: Translation vector + :return: Transformed copy + """ + new_segment = copy.deepcopy(self) + new_segment.apply_translation(vector) + return new_segment - 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.") +# ArcSegment ------------------------------------------------------------------ - # Private methods --------------------------------------------------------- +class ArcSegment: + """Arc segment.""" - def __init__(self, point0, point1, segment=LineSegment()): + def __init__(self, points, arc_winding_ccw=True): """ - Construct the shape with an initial segment. + Constructor. - :param point0: first point - :param point1: second point - :param segment: segment + :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 """ - point0 = np.array(point0) - point1 = np.array(point1) + 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): + raise ValueError("'points' is not a 2x3 matrix.") - Shape2D._check_segment(segment, point0, point1) + if arc_winding_ccw: + self._sign_arc_winding = 1 + else: + self._sign_arc_winding = -1 + self._points = points - self._points = np.array([point0, point1]) - self._segments = [segment] + self._arc_angle = None + self._arc_length = None + self._radius = None + self._calculate_arc_parameters() - @staticmethod - def _check_segment(segment, point_start, point_end): + def _calculate_arc_angle(self): """ - Check if segment is valid. + Calculate the arc angle. + + :return: --- + """ + 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) + 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 = tf.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 _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. + + :return: --- + """ + 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_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.") + + @classmethod + def construct_with_points(cls, point_start, point_end, point_center, + arc_winding_ccw=True): + """ + Construct an arc segment with three points (start, end, center). - :param segment: segment :param point_start: Starting point of the segment :param point_end: End point of the segment - :return: --- + :param point_center: Center point of the arc + :param arc_winding_ccw: Specifies if the arcs winding order is + counter-clockwise + :return: Arc segment """ - 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) + points = np.transpose( + np.array([point_start, point_end, point_center], dtype=float)) + return cls(points, arc_winding_ccw) - @staticmethod - def _check_segment_length_valid(point_start, point_end): + @classmethod + def construct_with_radius(cls, point_start, point_end, radius, + center_left_of_line=True, arc_winding_ccw=True): """ - Check if a segment length is valid. + 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 vector 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_with_points(point_start, point_end, point_center, + arc_winding_ccw) + + @classmethod + 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 + """ + 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): + """ + 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 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): + """ + 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 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): + """ + Get the radius. + + :return: Radius + """ + return self._radius + + def apply_transformation(self, matrix): + """ + Apply a transformation to the segment. + + :param matrix: Transformation matrix :return: --- """ - diff = point_start - point_end - if not np.linalg.norm(diff) >= Shape2D.min_segment_length: - raise Exception("Segment length is too small.") + self._points = np.matmul(matrix, self._points) + 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): + """ + 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 + :return: Array of contour points + """ + point_start = self.point_start + point_center = self.point_center + vec_center_start = (point_start - point_center) + + if not raster_width > 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 + + 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 = tf.rotation_matrix_z(angles)[:, 0:2, 0:2] + + data = np.matmul(rotation_matrices, vec_center_start) + point_center + + 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 ----------------------------------------------------------------- + +class Shape: + """Defines a shape in 2 dimensions.""" + + def __init__(self, segments=None): + """ + Constructor. + + :param segments: Single segment or list of segments + """ + segments = ut.to_list(segments) + self._check_segments_connected(segments) + self._segments = segments @staticmethod - def _check_segment_type_valid(segment): + 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 ut.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): + """ + Interpolate 2 shapes. + + :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 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(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, shape_a, shape_b, weight): + """ + Interpolate 2 shapes linearly. + + Each segment is interpolated individually, using the corresponding + linear segment interpolation. + + :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(shape_a.num_segments): + interpolation_schemes += [shape_a.segments[i].linear_interpolation] + + return cls.interpolate(shape_a, shape_b, weight, interpolation_schemes) + + @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): + """ + Get the shape's segments. + + :return: List of segments + """ + 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): """ - Check if the segment type is valid. + Add segments to the shape. + :param segments: Single segment or list of segments :return: --- """ - if not isinstance(segment, Shape2D.Segment): - raise TypeError("Invalid segment type") + segments = ut.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 - # Public methods ---------------------------------------------------------- + def apply_transformation(self, transformation_matrix): + """ + Apply a transformation to the shape. + + :param transformation_matrix: Transformation matrix + :return: --- + """ + for i in range(self.num_segments): + self._segments[i].apply_transformation(transformation_matrix) - def add_segment(self, point, segment=LineSegment()): + def apply_reflection(self, reflection_normal, distance_to_origin=0): """ - Add a new segment which is connected to previous one. + Apply a reflection at the given axis to the shape. - :param point: end point of the new segment - :param segment: segment + :param reflection_normal: Normal of the line of reflection + :param distance_to_origin: Distance of the line of reflection to the + origin :return: --- """ - point = np.array(point) + 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.") - Shape2D._check_segment(segment, self._points[-1], point) + dot_product = np.dot(normal, normal) + outer_product = np.outer(normal, normal) + householder_matrix = np.identity(2) - 2 / dot_product * outer_product - if self.is_shape_closed(): - raise ValueError("Shape is already closed") + offset = normal / np.sqrt(dot_product) * distance_to_origin - self._points = np.vstack((self._points, point)) - self._segments.append(segment) + self.apply_translation(-offset) + self.apply_transformation(householder_matrix) + self.apply_translation(offset) - def is_point_included(self, point): + def apply_reflection_across_line(self, point_start, point_end): """ - Check if a point is already part of the shape. + Apply a reflection across a line. - :param point: Point which should be checked - :return: True or False + :param point_start: Line of reflection's start point + :param point_end: Line of reflection's end point + :return: --- """ - return is_row_in_array(point, self._points) + 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 + + 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) - def is_shape_closed(self): + def apply_translation(self, vector): """ - Check if the shape is already closed. + Apply a translation to the shape. + + :param vector: Translation vector + :return: --- + """ + for i in range(self.num_segments): + self._segments[i].apply_translation(vector) + + 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) + """ + if self.num_segments == 0: + raise Exception("Can't rasterize empty shape.") + 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 = 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): + 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 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_end: 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. + + :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 --------------------------------------------------------------- + +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 is_row_in_array(self._points[-1, :], self._points[:-1]) + 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 of the shape. + Get the number of segments. - :return: number of segments + :return: Number of segments """ return len(self._segments) - def num_points(self): + 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 ------------------------------------------ + +def linear_profile_interpolation_sbs(profile_a, profile_b, weight): + """ + Interpolate 2 profiles linearly, segment by segment. + + :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](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): """ - Get the number of points of the shape. + Rasterize the geometry. - :return: number of points + :param profile_raster_width: Raster width of the profiles + :param trace_raster_width: Distance between two profiles + :return: Raster data """ - return self._points[:, 0].size + 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/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/mypackage/transformations.py b/mypackage/transformations.py new file mode 100644 index 0000000..6560aa5 --- /dev/null +++ b/mypackage/transformations.py @@ -0,0 +1,425 @@ +"""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 + + +# functions ------------------------------------------------------------------- + +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() + + +def normalize(vec): + """ + Normalize a vector. + + :param vec: Vector + :return: Normalized vector + """ + norm = np.linalg.norm(vec) + if norm == 0.: + raise Exception("Vector length is 0.") + return vec / norm + + +def orientation_point_plane_containing_origin(point, p_a, p_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 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(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([p_a, p_b, point])) + + +def orientation_point_plane(point, p_a, p_b, p_c): + """ + Determine a points orientation relative to an arbitrary 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 point: Point + :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 = 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(vec_u, vec_v, tolerance=1E-9): + """ + Check if vectors are orthogonal. + + :param vec_u: First vector + :param vec_v: Second vector + :param tolerance: Numerical tolerance + :return: True or False + """ + 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(vec_u, vec_v), 0, abs_tol=tolerance) + + +def change_of_basis_rotation(ccs_from, ccs_to): + """ + Calculate the rotatory transformation between 2 coordinate systems. + + :param ccs_from: Source coordinate system + :param ccs_to: Target coordinate system + :return: Rotation matrix + """ + return np.matmul(ccs_from.basis, np.transpose(ccs_to.basis)) + + +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 + + +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: + """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])): + """ + Construct a cartesian coordinate system. + + :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 + """ + basis = ut.to_float_array(basis) + 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._orientation = basis + + self._location = ut.to_float_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 CoordinateSystem(basis, origin) + + @classmethod + def construct_from_orientation(cls, orientation, + origin=np.array([0, 0, 0])): + """ + Construct a cartesian coordinate system from orientation matrix. + + :param orientation: Orthogonal transformation matrix + :param origin: Position of the origin + :return: Cartesian coordinate system + """ + 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 from 3 basis vectors. + + :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([vec_x, vec_y, vec_z]) + return cls(basis, origin=origin) + + @classmethod + def construct_from_xy_and_orientation(cls, vec_x, vec_y, + positive_orientation=True, + origin=np.array([0, 0, 0])): + """ + 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 + :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 + """ + vec_z = cls._calculate_orthogonal_axis(vec_x, + vec_y) * cls._sign_orientation( + positive_orientation) + + basis = np.transpose([vec_x, vec_y, vec_z]) + return cls(basis, origin=origin) + + @classmethod + def construct_from_yz_and_orientation(cls, vec_y, vec_z, + positive_orientation=True, + origin=np.array([0, 0, 0])): + """ + 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 + :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 + """ + vec_x = cls._calculate_orthogonal_axis(vec_y, + vec_z) * cls._sign_orientation( + positive_orientation) + + basis = np.transpose(np.array([vec_x, vec_y, vec_z])) + return cls(basis, origin=origin) + + @classmethod + def construct_from_xz_and_orientation(cls, vec_x, vec_z, + positive_orientation=True, + origin=np.array([0, 0, 0])): + """ + 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 + :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 + """ + vec_y = cls._calculate_orthogonal_axis(vec_z, + vec_x) * cls._sign_orientation( + positive_orientation) + + basis = np.transpose([vec_x, vec_y, vec_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 _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 a_0: First axis + :param a_1: Second axis + :return: Orthogonal axis + """ + return np.cross(a_0, a_1) + + @property + def basis(self): + """ + 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._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._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) +# 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/mypackage/visualization.py b/mypackage/visualization.py new file mode 100644 index 0000000..3a9cb35 --- /dev/null +++ b/mypackage/visualization.py @@ -0,0 +1,73 @@ +"""Contains some functions to help with visualization.""" + +import numpy as np + + +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 + 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") + 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): + """ + 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]) diff --git a/setup.cfg b/setup.cfg index 9555dce..ee721b1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -4,10 +4,12 @@ # 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 [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 new file mode 100644 index 0000000..2bbc3a3 --- /dev/null +++ b/tests/_helpers.py @@ -0,0 +1,40 @@ +import mypackage.transformations as tf +import numpy as np +import math + + +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) + + +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.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 d85f7bb..224e0db 100644 --- a/tests/test_geometry.py +++ b/tests/test_geometry.py @@ -1,65 +1,2045 @@ -import pytest import mypackage.geometry as geo +import mypackage.transformations as tf +import mypackage._utility as utils + +import tests._helpers as helpers + +import pytest +import numpy as np +import math +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 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, + 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 + 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] + 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 that there are no duplicate points + assert helpers.are_all_points_unique(data) + + # 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 + helpers.check_vectors_identical(point_start, data_200[:, 0]) + helpers.check_vectors_identical(point_end, data_200[:, 1]) + + # check exceptions when raster width <= 0 + with pytest.raises(ValueError): + segment.rasterize(0) + with pytest.raises(ValueError): + segment.rasterize(-3) + + +# test LineSegment ------------------------------------------------------------ + +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([[[3, 5], [3, 4]]]) + + # factories ------------------------------------------- + segment = geo.LineSegment.construct_with_points([3, 3], [4, 5]) + assert math.isclose(segment.length, np.sqrt(5)) + + +def test_line_segment_rasterization(): + raster_width = 0.1 + + 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(points) + + # perform default tests + default_segment_rasterization_tests(segment, raster_width, point_start, + point_end) + + # 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] + + 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) + + +def test_line_segment_transformations(): + # translation ----------------------------------------- + segment = geo.LineSegment.construct_with_points([3, 3], [4, 5]) + segment_2 = segment.translate([-1, 4]) + + # 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.) + c = np.cos(np.pi / 4.) + rotation_matrix = [[c, -s], [s, c]] + + segment = geo.LineSegment.construct_with_points([2, 2], [3, 6]) + 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_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_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])) + + # 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_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])) + + # 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_2.length, np.sqrt(145)) + + # apply same transformation in place + segment.apply_transformation(scale_matrix) + check_segments_identical(segment, segment_2) + + # exceptions ------------------------------------------ + + # transformation results in length = 0 + 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(): + 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 + 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) + + # check weight clipped to valid range ----------------- + + segment_c = geo.LineSegment.linear_interpolation(segment_a, segment_b, -3) + 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) + 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 ------------------------------------------ + + # wrong types + 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): + geo.LineSegment.linear_interpolation(arc_segment, segment_a, weight) + with pytest.raises(TypeError): + geo.LineSegment.linear_interpolation(arc_segment, arc_segment, weight) + + +# test ArcSegment ------------------------------------------------------------ + +def check_arc_segment_values(segment, point_start, point_end, point_center, + winding_ccw, radius, arc_angle, arc_length): + 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.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) + point_start = np.array(point_start) + point_end = np.array(point_end) + + radius_arc = np.linalg.norm(point_start - point_center) + + arc_segment = geo.ArcSegment.construct_with_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, + 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) + + +def test_arc_segment_construction(): + points = [[3, 6, 6], [3, 6, 3]] + segment_cw = geo.ArcSegment(points, False) + segment_ccw = geo.ArcSegment(points, True) + + 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) + + 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) + + 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 + points = [[3, 6, 6], [3, 10, 3]] + with pytest.raises(Exception): + geo.ArcSegment(points, False) + + # radius is zero + points = [[3, 3, 3], [3, 3, 3]] + with pytest.raises(Exception): + geo.ArcSegment(points, False) + + # arc length zero + points = [[3, 3, 6], [3, 3, 3]] + with pytest.raises(Exception): + geo.ArcSegment(points, False) + with pytest.raises(Exception): + 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]]]) + + +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] + + # 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 + + segment_cw = geo.ArcSegment.construct_with_points(point_start, point_end, + point_center_right, + False) + segment_ccw = geo.ArcSegment.construct_with_points(point_start, point_end, + point_center_right, + 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) + + # 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(): + # 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) + + +def test_arc_segment_transformations(): + # translation ----------------------------------------- + segment_cw = geo.ArcSegment.construct_with_points([3, 3], [5, 5], [5, 3], + False) + segment_ccw = geo.ArcSegment.construct_with_points([3, 3], [5, 5], [5, 3], + True) + + 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] + 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_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.) + c = np.cos(np.pi / 4.) + rotation_matrix = [[c, -s], [s, c]] + + segment_cw = geo.ArcSegment.construct_with_points([3, 3], [5, 5], [5, 3], + False) + segment_ccw = geo.ArcSegment.construct_with_points([3, 3], [5, 5], [5, 3], + True) + + 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_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) + reflection_matrix = np.identity(2) - 2 / np.dot(v, v) * np.outer(v, v) + + segment_cw = geo.ArcSegment.construct_with_points([3, 2], [5, 4], [5, 2], + False) + segment_ccw = geo.ArcSegment.construct_with_points([3, 2], [5, 4], [5, 2], + True) + + 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_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]] + + segment_cw = geo.ArcSegment.construct_with_points([3, 2], [5, 4], [5, 2], + False) + 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) + + 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.construct_with_points([8, 4], [32, 4], [20, 2], + False) + 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) + + 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.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) + + # transformation results in length = 0 + 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) + + +def test_arc_segment_interpolation(): + 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): + geo.ArcSegment.linear_interpolation(segment_a, segment_b, 1) + + +# test Shape ------------------------------------------------------------------ + +def test_shape_construction(): + 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() + assert shape.num_segments == 0 + + # Single element construction shape + shape = geo.Shape(line_segment) + assert shape.num_segments == 1 + + # Multi segment construction + 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) + + # exceptions ------------------------------------------ + + # segments not connected + with pytest.raises(Exception): + shape = geo.Shape([line_segment, arc_segment]) + + +def test_shape_segment_addition(): + # Create shape and add segments + 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) + assert shape.num_segments == 1 + + 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_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 + + +def test_shape_rasterization(): + points = np.array([[0, 0], + [0, 1], + [1, 1], + [1, 0]]) + + raster_width = 0.2 + + shape = geo.Shape( + geo.LineSegment.construct_with_points(points[0], points[1])) + shape.add_segments( + geo.LineSegment.construct_with_points(points[1], points[2])) + shape.add_segments( + geo.LineSegment.construct_with_points(points[2], points[3])) + + data = shape.rasterize(raster_width) + + # 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) + + 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) + 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(): + # create shape + 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]) + + +def test_shape_translation(): + def check_point(point, point_ref, translation): + helpers.check_vectors_identical(point - translation, point_ref) + + translation = [3, 4] + + shape_ref = default_test_shape() + + # apply 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] + + assert (arc_segment.arc_winding_ccw == arc_segment_ref.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) + + # apply same transformation in place + shape_ref.apply_translation(translation) + check_shapes_identical(shape_ref, shape) + + +def test_shape_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() + + # apply transformation + 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] + + 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) + 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) + + # 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_ref = default_test_shape() + + # apply transformation + 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] + + assert (arc_segment.arc_winding_ccw != arc_segment_ref.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) + + # 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): + """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 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 + offset = distance_to_origin * unit_normal + + shape = default_test_shape() + + # create reflected shape + 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] + line_segment = shape.segments[1] + line_segment_ref = shape_reflected.segments[1] + + # check reflected points + 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) + + 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) + + # apply same reflection in place + shape.apply_reflection(normal, distance_to_origin) + check_shapes_identical(shape, shape_reflected) + + +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) + + # exceptions ------------------------------------------ + shape = default_test_shape() -def test_shape2d_construction(): - # Test Exception: Segment length too small with pytest.raises(Exception): - geo.Shape2D([0, 0], [0, 0]) + 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]) + shape_reflection_across_line_testcase([-2, 1], [2, -4.5]) + + # exceptions ------------------------------------------ + shape = default_test_shape() - # Test Exception: Invalid point format with pytest.raises(Exception): - geo.Shape2D([0, 0], [1]) + shape.reflect_across_line([2, 5], [2, 5]) with pytest.raises(Exception): - geo.Shape2D([0, 0, 4], [1, 1]) + shape.apply_reflection_across_line([-3, 2], [-3, 2]) + + +def interpolation_nearest(segment_a, segment_b, weight): + if weight > 0.5: + return segment_b + return segment_a + + +def test_shape_interpolation_general(): + 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_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, + 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_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: + check_segments_identical(shape_c.segments[1], segment_a1) + + +def test_shape_linear_interpolation(): + 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_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): + weight = i / 4. + shape_c = geo.Shape.linear_interpolation(shape_a, shape_b, 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]) + + 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) + + 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) + + 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) - # Test Exception: Invalid shape type + # exceptions ------------------------------------------ + + shape_a.add_segments(geo.LineSegment.construct_with_points([2, 0], [2, 2])) with pytest.raises(Exception): - geo.Shape2D([0, 0], [0, 1], "wrong type") + geo.Shape.linear_interpolation(shape_a, shape_b, 0.25) - # Create shape - geo.Shape2D([0, 0], [0, 1]) +# Test profile class ---------------------------------------------------------- -def test_shape2d_boolean_functions(): - shape = geo.Shape2D([0, 0], [0, 1]) +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]) - # Point included or not - assert (shape.is_point_included([0, 1])) - assert (not shape.is_point_included([5, 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]) -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]) + # 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 - # Test Exception: Invalid point format + # exceptions with pytest.raises(Exception): - shape.add_segment(0) - # Test Exception: Invalid shape type + profile.rasterize(0) with pytest.raises(Exception): - shape.add_segment([0, 0], "wrong type") + profile.rasterize(-3) - # Close segment - shape.add_segment([0, 0]) - # Test Exception: Shape is already closed - with pytest.raises(ValueError): - shape.add_segment([1, 0]) +# 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) - # Number of segments has to be one less than the number of points - assert shape.num_segments() == shape.num_points() - 1 + # 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_shape2d_with_arc_segment(): - # Invalid center point + +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.Shape2D([0, 0], [1, 1], geo.Shape2D.ArcSegment([0, 1.1])) + 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) - 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 + # 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): - shape.add_segment([3, 1], segment=geo.Shape2D.ArcSegment([2.1, 1])) + 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.linear_profile_interpolation_sbs(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.linear_profile_interpolation_sbs(profile_a, profile_b, -3) + + check_interpolated_profile_points(profile_c, a_0, a_1, a_2) + + profile_c = geo.linear_profile_interpolation_sbs(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.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), + geo.LineSegment.construct_with_points(b_1, b_2)]) + + profile_b2 = geo.Profile([shape_b01, shape_b012]) + with pytest.raises(Exception): + geo.linear_profile_interpolation_sbs(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.linear_profile_interpolation_sbs + + 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.linear_profile_interpolation_sbs + + 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.linear_profile_interpolation_sbs) + + 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.linear_profile_interpolation_sbs + + 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_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 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") diff --git a/tests/test_trasformations.py b/tests/test_trasformations.py new file mode 100644 index 0000000..f3a637b --- /dev/null +++ b/tests/test_trasformations.py @@ -0,0 +1,429 @@ +import mypackage.transformations as tf +import numpy as np +import pytest +import random +import math +import copy +import tests._helpers as helper + + +# 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 + 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): + assert np.linalg.det(matrix) >= 0 + + +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_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], + 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(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 + 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 np.transpose([x, y, z]) + + +# test functions -------------------------------------------------------------- + +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 + + +def test_normalize(): + for _ 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])) + + +def test_orientation_point_plane_containing_origin(): + [a, b, n] = rotated_positive_orthogonal_base() + a *= 2.3 + b /= 1.5 + + 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)) + + # 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 + + +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 + + +def test_is_orthogonal(): + 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) + 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]) + + +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), + 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.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) + + check_matrix_identical(matrix, ref_mat[i]) + + +def test_change_of_basis_translation(): + 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()) + base_to = rotated_positive_orthogonal_base(*random_non_unit_vector()) + + 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) + + expected_diff = origin_from - origin_to + for j in range(3): + 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(): + # alias name for class - name is too long :) + cls_ccs = tf.CoordinateSystem + + # setup ----------------------------------------------- + origin = [4, -2, 6] + basis_pos = rotated_positive_orthogonal_base() + + x = basis_pos[:, 0] + y = basis_pos[:, 1] + z = basis_pos[:, 2] + + basis_neg = np.transpose([x, y, -z]) + + # construction with basis ----------------------------- + + 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) + + # 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 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]]) + + +def test_cartesian_coordinate_system_addition(): + cls_ccs = tf.CoordinateSystem + + 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) diff --git a/tests/test_utility.py b/tests/test_utility.py new file mode 100644 index 0000000..b29f8b9 --- /dev/null +++ b/tests/test_utility.py @@ -0,0 +1,56 @@ +"""Test the internal utility functions.""" + +import numpy as np +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]) + + 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]) diff --git a/tests/test_visualization.py b/tests/test_visualization.py new file mode 100644 index 0000000..1663ab6 --- /dev/null +++ b/tests/test_visualization.py @@ -0,0 +1,35 @@ +"""Test functions of the visualization package.""" + +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 + + +def test_plot_coordinate_system(): + cs = tf.CoordinateSystem() + fig = plt.figure() + ax = fig.gca(projection='3d') + + 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(): + fig = plt.figure() + ax = fig.gca(projection='3d') + vs.set_axes_equal(ax) diff --git a/tutorials/geometry_01_profiles.ipynb b/tutorials/geometry_01_profiles.ipynb new file mode 100644 index 0000000..c9226c2 --- /dev/null +++ b/tutorials/geometry_01_profiles.ipynb @@ -0,0 +1,1202 @@ +{ + "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 tf" + ] + }, + { + "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], 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", + "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], 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", + "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], 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", + "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 the 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(reflection_normal=[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(reflection_normal=[0, 1], distance_to_origin=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(reflection_normal=[-1, 1], distance_to_origin=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": "iVBORw0KGgoAAAANSUhEUgAAAXIAAAD4CAYAAADxeG0DAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4xLjEsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy8QZhcZAAAgAElEQVR4nO29e3jU1bX//9ozuRFyEbkrIEHukBBIkIBFCJpqkVKxQkFUxPoAnp6eY71gLRV+Qi0KSHuqPWi1arHIzV/1KNIWU5Oigsr9FkJAiFzkqiQmgSSTmf39Y/PJ5E5CJvnMTNbreeaZmc9l7zU7yXt21l5rbaW1RhAEQQhcHHYbIAiCIDQOEXJBEIQAR4RcEAQhwBEhFwRBCHBEyAVBEAKcEDs6bdeune7evbsdXTcLRUVFtG7d2m4zbEfGwSDjYJBxMDRmHLZt23ZOa92+6nFbhLx79+5s3brVjq6bhczMTEaPHm23GbYj42CQcTDIOBgaMw5Kqa9qOi6uFUEQhABHhFwQBCHAESEXBEEIcETIBUEQAhwRckEQhABHhFwQ/JFFiyAjo/KxjAxzXBCqIEIuCP7GokUQEgKTJsHMmUbAly6FceNg6FARdKEatsSRC4JQB0OHGhF/8kmYPx/eeANcLliyxJyfNAnWrLHVRMG/kBm5IPgT1kx7zRpYuBASEqC0FJSCvLzKIi6zcuESIuSC4C9UdKkA/OAH8PHHRsQ9HliwAB56yJybNMlcK2IuIEIuCP6BJeILFxqXyg9/CG++ac6Fh0NoqHn929/CHXeYaxYuFDEXABFyQbAXKzpl6FCviM+dC0VF5nxSEoSFQWQk9O4NbjcUFsLTT4uYC+WIkAuCXVR1paxZA0895RXx+HjIzoZ584y4Hz8OcXHGzeLxeIVfxLzFI0IuCHZQ1ZUyaZJ5feGCOR8fDydPmqiV+fPNDHzBAigogE6dzKy8uNics8RcQhNbLCLkgmAHFV0pCxdC587w4YfmXFqaEXHr3MiR5mFdX1bmFfPvvjNibkWyTJpk2hZaFCLkgtDcVA0x7NwZ9uwxx1q3NmJtnXvySSPiN93kFXZLzOPiQGsj6BkZEprYghEhF4TmpKpffPDgyiI+f35ln3lZGcyebR5lZZXFvKDALIa63RKa2MIRIReE5qKmEEPLnRIZaUTcOrdlC6SmGgG3qCrma9bA4sUSmiiIkAtCU9N15cq6QwzT0mDduspuk4oCXhFLzCv6xP/5T+/MXEITWyQi5ILQlCxahHY66w4x3LHDe64uEbeYPdvM1rds8Qr6V19JaGILRoRcEJqSoUPp9tZb3hDD11+vHmJonYPLi3hFrGutAlsVQxMrunCs0EQhaBEhF4Sm4tIsOGvePCOm/fp50+6rhhhafvGGsmVL9WiWTp3g1CmvmK9ZY2bwEmMetPikjK1S6jVgHHBGaz3QF20KgcuK06eZc/gwR4FumzfzTI8eTO3Y0W6zmpexY6F7d5g/ny7x8SY6xVrYDAmBY8cgJQXmzIHoaFi9GhITTf1xgI0bTchhRWo7FhMDPXuathITjbi3amXEPCbGXPfDHxohv+8+Y9v69U368YXmxVf1yN8AXgSW+6g9IUBZcfo0Mw4c4ILHA8BXJSXMOHAAoGWJudMJy5bBiBG03bSp8rmyMpN6n5trFijPnjVukb17TaXDsjIoKTHnHY7LH4uIMO0AfPaZeb540Tx/9x2MGWOuHz7c2HT77c00CEJz4RPXitZ6I/CtL9oSAps5hw9zweOhNYXcy3IcuLng8TDn8GG7TWteHnnEVC2sKuIWSpnNIsrKjJuluNiIsctl6o839JjDYRY5wXyJVCUkxNgSEQGPPtp0n1uwBaW19k1DSnUH1tXmWlFKzQBmAHTs2DFp1apVPunXHyksLCQqKspuM2xhDKCBm0nn1zzDq/yUFdyDAj6y2bbmpOvKlWink+uXLUNhxkRdOqcdDtQl0c2Lj+eqPXvKnxt7rDQqirDCQgrj4og6csT0d6lv7XDw5cyZKLebY1OmNNNIeGnJfxcVacw4pKambtNaJ1c7obX2yQPoDuytz7VJSUk6mMnIyLDbBNu4btMmTUaGJuMj/euMMTo9w6EHZLygr9u0yW7TmpePPtI6PFxrk0Rf8yMkxDz36mWew8O1DgvTWimt09Iadszp1NrhMK979669z4gIY5sNtOS/i4o0ZhyArboGTZWoFcGnPNOjB5EOB6D4Hb/gNB15imf47XVt7TateVm61PivMTPiaoSGGncHwMGDxuXhdJrjYWFmYbQhx0JCjGvF6YScnNrtKi6G55/3+ccV7EU2XxZ8irWgOefwYY6WwKuh83nK9RDXFCwGXrHXuObk88/Ns9OJcruN0JaVmWMOB7RvD99+C+PGwa5dpnBWYqL3/oZGrXToYN6npxsxLy31XtO7txF3y4/+xRe+/ayC7fgq/HAlMBpop5Q6DszTWv/ZF20LgcfUjh2Z2rEjmZmZjL7xQc6ciSY29nt2m9W8TJgAf/4zuN14nE4cloi3awfnzsHXX5sMz9RUk9Dzk5/UL6uzKhXrt6xZY/4LsMIcnU6zqJqTY5KP9uwx106Y4NvPKtiOr6JWpmitO2utQ7XWXUTEhYp06PATwsOvRWsPpaVn7Dan+bjkBnFYoYERESZ2vGKRqwkTrjyVvmoRrnHjvCLeurXJ5iwrM9fs2WOiaMLCfPsZBb9AfORCs7F//1R27UrD7S6225TmQWuvHzw01LxPT69c5MraGKIhYl7bPp9W6n9aGrz/vqnhEhZm+rXsUar2doWARYRcaDY6dryXoqLdHD7cQPdBoOLxQEkJefHxJu7b7YZbbjHnrCJXWpvj9S1yVdM+nxUrKVYswnXLLaZtj8dsTmHFrQtBhwi50Gy0bTuWLl0e5sSJFzh37j27zWlaNm40ESJpaSbGOy3NiOiuXTUXuSourlx+tqb9N2vb57OiiFcswrVnj/fL4+OPzXNJibFNCCpEyIVmpUePZ4mKSiQ7ezolJSfsNqfpuOkm45NOTzcz8vR0875z55qLXBUWemuJ17b/ZkP2+XzySdNuRIS5ZuRIb6hi1cgXIeARIReaFYcjnP79VxEW1in4Fz5DQiA01MzIrbjxxMTqW7ZZ+296PGZ2XdP+m3Xt8xkZWX2fz7Iy05fDYb5APv7YPDvkTz4YkZ+q0OxERvZh6NA9REcPttuUpqXi4qJS3kXHhu6/uXGjEeLa9vlcsKDmfT6r9iuLnUGLCLlgC0o58HhKOXToMfLzayksFehUXOwsKfEWtYL67b85YQIMG2Zm6BkZJnSxaojh5fb5LCszi5yy2BnUiJALtuHxFHPu3N/IyroblyvPbnN8S02LncXFlRcaL7f/Zn4+ZGZ6Z9X//Gf1EMO69vncuNF8gchiZ9AjQi7YRkhIDP37r6S09AQ5OTOs4mvBQW2LnVUXGmvaf3P/fmjTxrx2uYyIWzNrMP50qzxuXft83nSTLHa2EETIBVuJiRlGXNxvOHt2LSdPBllCcE2LnbVRcf/NadMgL8+IeWmpmYVbtVPi4szmEvfdV799PmWxs0UgP1XBdrp2fZw2bW7h8OEnKCsrtNsc37Bzp3GPVFzsdLvN8drYssX4xFesgFmzjJiHh3vPWyI+axa89Zbxmde1z+fOncYvX3Gx0+Op2wYhIBEhF2xHKQd9+y4nMfHfhIQEycYDP/mJ8YlXXOwsLjbH68IS/BUrYMiQ8lK4ABw5AvfcY0TccrnUx4bSUuNaKS2tnw1CwCFCLvgF4eGdiYoym0t9991Wm63xAatXG390WJhxrYSFmferV9d8vZW1+dlnxmd+8SJs22bOhYZ6XSIrVhifeGqqKZVbVzq/ZUNF10pdNggBiwi54FecPr2S7duHcu7c+3ab0jisZJyKrhWHo3LNcai5AFZGhndhMykJWrWC6Gjo0sW4Ri5cgE8+uXxtlsREU8q2omvF6axugxDwiJALfkX79ncSFTU4OFL464ojh/oVwMrOhnnzzPFvv214oS2JI28RiJALfoWVwu/xFLN//z1o7bbbpCvjcnHk9S2ANX++eTz9tMngbEihLYkjbzGIkAt+R2Rkb3r1epG8vEyOHn3WbnOujMvFkTekANbIkebR0EJbEkfeYpA9OwW/pFOnaRQUfE6rVn3sNuXKCQkBrb2LnVYc+aJFRmzXrDHCW1MBLPCWu7XcIWPGVBb/uDgTyWIV2lq2rHKhLZA48haC/FQFv0QpRe/ey+jQ4S6AwMz6rKlo1s6dlf3i9S2A1dBCWyEhpi8pmtUiECEX/J4TJ/5Idva0wBPzqoudpaVmRlzbHpuXK4BV30Jb1nmHw/jRZbEz6BEhF/wet7uQ06ff5NSp1+w2pf5UXexMSjIiumVL7Xts1lUAy+JyhbYq7gH6xRfmWM+estgZ5IiQC36PlcJ/8ODPKSrab7c59cNa7PzwQy526mSSe0JD4Qc/qH2PzboKYFWkpkJbNe0Bai16HjwI3bvLYmcQI0Iu+D1WCr/T2ZqsrMm43cV2m1Q/PB5wOGh16pRxc7hcRrRr22MTLi/iFalYaKumPUC3b/e6XXJzzWt3gIZzCnUiQi4EBOHhnenb9y8UFe3l/Pl0u82pHw6HNwnI4zELjScuJTnVtMdmXQWwasNy1dQUmuhymYcl5i6XyewUgg6fhB8qpW4D/gdwAq9qrQM0+FfwZ9q2HcsNNxwgMrKn3aZcHqvyIKCVQmntjR4JDYVjx0z1wjlzTPr96tUmdX7mTHPNxo3VXSC1HYuJgeuvh1/9yrSxZQtERRkxByPgVvTK5SowCgFJo4VcKeUE/gikAceBLUqp97TWWY1tWxCqYon4+fOZREb2Ijz8WpstqgWljID27o3Kyal8zuUyqfdHjhixP3vWLFLu2+ctNVtcbM5fikWv81h4uHmvtSmkBV4Rt9AaeveGnByJJQ9CfPETvQE4pLU+rLUuBVYBP/JBu4JQIy5XHnv3jmf//nv9N4X/zjth/HjIyaFS0KQVx+1weOugpKWZ0ESXy8yYL0W7NOgYeGf8VuJRVcHOyTE2TZjQlJ9csAHV2NhcpdRdwG1a6wcvvb8XGKa1/s8q180AZgB07NgxadWqVY3q158pLCwkKipI6mo3gqYdh38AzwE/Be5poj6unKt27GDAU08RcuECSmtKr7qKsDyzL+nFTp3MAiiQFx/PVXv2lD839pjVdmFcHFFHjgDgat2a0KIiPA4Hnlat2LtgAXmDBzfreID8XVg0ZhxSU1O3aa2Tq53QWjfqAUzE+MWt9/cCL9R1T1JSkg5mMjIy7DbBL2jKcfB4PHrfvik6I8Op8/I+bbJ+rpjnntM6JUVr0AVxccbx4XRq7XCY12FhWoeHm9dpaVor5dtjYPpyOs3r+HhzXUqKsc0G5O/C0JhxALbqGjTVF66V40DXCu+7AF/7oF1BqBUrhT8iohtZWXdTVvad3SZVJiTE+KsfeoiIM2dM/Lbb7Y0acTjMa6uoVViYWQT11bHwcHPc7TbHvvrKbBFnbUYhBBW++IluAXoppeKAE8Bk4G4ftCsIdRISEkv//ivJz/8Up9PP/mVPT4clS6CsjNOpqVz7yCOwdCns2gUPP+yNUoGGRag05NjOndChAzzyCKxaZZKCliwxtj3yiE8/rmAvjRZyrXWZUuo/gX9iwg9f01rva7RlglAPYmKGERMzDACPpxSHI8xmiy6xfn35y4M33MC1o0ebbEyL5hZSO/sWmhyfxCFprddrrXtrra/XWj/jizYFoSHk5f2bzz7rETgp/ILgQySgVAgKWrXqjdYlgZXCLwg+QoRcCAq8Kfy7OXy4AfVKBCEIECEXgoa2bcfSpcvDnDjxAufOvW+3OYLQbIiQC0FFjx7PEhWVyPnzH9ptiiA0GxJQKgQVDkc4iYn/JiQkxm5TBKHZkBm5EHRYIl5UlMXJkwG0q5AgXCEi5ELQcuzYYg4cmEF+/ia7TRGEJkWEXAhaevb8fXkKv8uVZ7c5gtBkiJALQYuVwl9aeoKcnBlWUTdBCDpEyIWgJiZmGN27L+Ds2bWcPbvWbnMEoUmQqBUh6OnWbTYhITG0ayf7nQjBiczIhaBHKQfXXvsfOBzhuFx5ksIvBB0i5EKLoaysgG3bhkgKvxB0iJALLYaQkGjatfuRpPALQYcIudCisFL4s7OnU1Jywm5zBMEniJALLQqHI5z+/Vfh8Vxk//570dptt0mC0GhEyIUWR2RkH3r1ehEwfnNBCHQk/FBokXTqdD+dOk1DKZnLCIGP/BYLLRKlFEo5KCk5yf7990sKvxDQiJALLZqSkqOcObNCUviFgEaEvIUzdiwsXVr52NKl5nhLoGIK/6lTUvJWCEzER95CWbQIhg6FW26BRx+FQ4fA7YZdu+CLL2DWLO81W7bA7CDOoenWbTZ5ef/i4MH/IiZmBK1b97PbJEFoEDIjb4EsWgQhIfDDH8JHH8H48bBsGbzyCnz+ObRvD6++Crm5MGmSuXbRIsjIMM/BhlIO+vZdjtMZKVmfQkDSKCFXSk1USu1TSnmUUsm+MkrwPYsWwcyZRoyHDoWFC+Hmm+GDD+D9S0mOlov4zBno2NGIe8+eMGeOV9SHDjVtzJwZXKIeHt6Z+PgP6Nt3ud2mCEKDaeyMfC9wJ7DRB7YITYA1kx46FFatggkTYMcOSEmB994DpbwCbhEaCsePQ1ycmaG73fDSS+aepUvNTH7VquAT9ZiYGwgNbYPHU0ph4R67zRGEetMoIdda79daH/CVMYJvsVwokyaZ9+++CyUlxif+97+Dw1FdxAFcLmjTBo4cMTNzlwvCwuDDD2HdOigqgptuCl5Rz8mZxc6dqZLCLwQMstgZZCxaBF9+CZMnG3GdNAkmTjRRKP37Q/GlCq7uS5npkZFw4ULlNtq3h7NnzZfAqVPQqxecOOG9LjzciHpJiXk/bpwR9YwMcDrNF0ZGBixd2psvvgi8hdJu3Z7gzJk17N9/L4MGfYhSTrtNEoQ6UZeLnVVKpQOdajg1R2v9f5euyQQe01pvraOdGcAMgI4dOyatWrXqSm32ewoLC4mKimrWPleu7Erfvibd/KmnBgCK++7LZceOWD77rB0OB3g8CqfTg8OhcbkcKKXRWl1yryjA+7vgdGpA43Qa0Xc4wOVyEBrqAcDlMuLmfa8ASEk5x+DB+Sxf3h2tPfzmN1kAZGdHM2XKseYZDJ/wD+A54KfAPY1qyY7fB39ExsHQmHFITU3dprWuvh6ptW70A8gEkut7fVJSkg5mMjIymrW/557T+vnntW7XTuuPPjKPiAitQevwcK1DQ81rh0NrpbTu21fr8ePNMaUqP8fEmGfQunVrrR96yNwfHm76uP127/nwcPOo6X1oqNYpKWeq2TVjhrHX3/F4PHrfvik6I8Op8/I+bVRbzf374K/IOBgaMw7AVl2DpoprJUCprwvFcn8MGQL79sEDD8Bbb8HXX8OIEbBpE7RqBRcvwoAB5ppOnYxL5ZprYPlySEqChAQYPBjmzzfumNRU0+4HH5jn8PDK/TkcsG1bWz7/HG6/vWbXy6pVcP31/ul6UUrRu/cy3O4CnM4Yu80RhDpplJArpSYALwDtgQ+UUju11rf6xDKhRqwknaFD4ZlnYPVqmDvXRJS89JIR0O3bTeSJw2GENSICliwx90+aBCNHwrlzRsTj4syiZu/eRsQtMY+Lg4MHTYz5pk3w29+axKCf/MR8eYCJgKlL1F0uR7396f4o6iEhscTHywYUgv/T2KiVd7TWXbTW4VrrjiLiTUtdUSgffmjOWf7ssjKTtfn88ybiZMIEc8+ddxpHyJ49ZqZ95IgR7ZwcM0PPzTWLm9bx996DJ5/02vDyy0a4LVFft870/8knRtRvv930W1oKoaFmRdUScUvUrciX2FhYu9bYtmqVsX/cOP8rD+B2F5OdPZ2TJ/9stymCUCOS2RlADB1qZt8TJxoxf/31yi4Ul8u4UEJDTYr9558bd8i77xrR3bLFzKY//9wkA23b5p2Rjx8PO3fCfffB6dPmy+DIEUhLgwMHKmd4gpk51ybqn39u/gNISjqPx6yFVnO9hIbCsWMm6ejCBfNfwty5Zlbv9LMgEYcjjOLiYxw8+F8UFe232xxBqE5NjvOmfshi55Xz/PNmYTI+3iwqOp3excaICO+iYrt25tqqC4vPPWcWMEHruDjznJSkdWSkOd66tbkvMrLyomfFRcu6eO45rw2tW7t0TEz1RVJrMbTie4fD29fl+rCD4uKv9SeftNNffDFIl5VdbNC9sshnkHEwNMVip8zIA4yyMjPr3rPHZGW63dCvn3FJVHShrFljrq3qcx461LuAeeQIxMcbn/q0acatMn++Sd+/5x4zGwezEDp/vnGxbNlSt32zZ3tn6ampp5k82fjAMzPNrDwsDLp2Nf89VMTjMTPxqVO9Pnd/Ijy8M337/oWiol1Sj0XwO0TIA4zcXOMSiYoyc9nevSE724jj5MleF0pqanURt9wi991n2oiPN18Iw4YZX/XjjxvxX7MG+vQx/uy4OCOyHToYga/oXrkcY8acZfJkSE83PvMHHjBfQsdqCSd3u2H37isemianbduxdOnyMKdOLae09LTd5giCl5qm6U39ENfKlfHRR8blMWJEZdfIiBHm+OVcEjNmGNdFRITWaWleF4cVI265YSzXjBVrbvUzfnz93SvPP691bGxJtbh2p9PrRqnoVrHi2MHc66+43cX64sXcBt0jLgWDjINBXCstnC1bYMECE2FSMbJk0yZzHOqeLU+e7E3N/+QT4+ooKzMz5YpumC1bjBtl06baI1iq9lNTdcVRo87ygx/AY49VLg3g8Zh2HZd++zweM9NPSTGLoKtX+2a8mgKHI5yIiOvQWnPmzFq0dtttkiCIkAcSs2cbwX3ySVNq9nKRJRWxjj3zjBHVixfN+7FjjVtl6NCa+7lcBEvF6oqrV8Mdd5jqisOGwXvvXYPL5Y1rtyJXQkJMuxX7dLlMNM4//wk//nHTjJ8vOX/+Q7KyJnH06LN2myIIIuSBhjXbnTvXZGSC8WX/9a9GeBcurCyQFl9+aUQ2I6N6KOCaNdUXMa1+5s0zCUVWPytWePv58svKce3vvGN84Y8+avziTqcuX8QsKzNfBA89ZGblFy+aMEUr1DAiwiyogn8lBdVGmzZpdOgwhSNH5pGfv8luc4QWjgh5gGG5PRYuNBEeVmRJcTE8/XTtkSWWW2XdOvPeEvOMDPNcVTwr9rNunVmkBBPz/ctfmnN9+njj2qu6UEpKwO1WleLaP/vM2DBrlnm2om7S0swCrNYmMSgQsFL4IyK6kZV1Ny5Xnt0mCS0YEfIAw3J71BRZ0rFjzZEl1msrrM+aid9+uxHZmsSzYj9g9vK0NqHweIyA/+pX0LatKQ1QkwslNNTDkiWwfr1x31j+9bVrTTq+1iZyJj3dfJZ33zXHA4WQkFj6919JaekJcnJm2W2O0IIRIQ9ArNnzwoUmI9NajDx40KTZV3WvWG4VqO5Weeed2sXT6mfcOOPz1prysrZFRaaNkyfNF0dVF4opDaDL+12zxgj25MlmBn/woBHxvXtN2wsXVu4zUIiJGUbPnn+gc+cH7TZFaMGIkAcoDYksaahbxbp35kwzW582DTZvNrHq7ipBGi5X9dIAn31mSgMsWLCXyZO9ce1du5odhV5/3Yj9nj3mS2DnzvolG/kr1177EFdffQsAHk+pzdYILRER8gClvhEsVgGq+rpVqkahrFplEniGDaueyKPMXhKEhEBycmUXypYtMHhwHi+/7P2imDzZ2Azw6afe8Mfp02vOQg00jh17nu3bU3C7i+02RWhhiJAHMPWJYHE66+9WqVpd0YpCWbfOZIJWRWvjynG7TTQL1F4awPrv4Le/NQui1rZxt99ePfwxUImM7Edh4Q5J4ReaHRHyAKZiZMn773sjS6wIljVrzEz84sXa3SpQPZGnpigUqzZKr17embhlw5IlMHo0LF5cc2kAMH76CRPgo4/qF/4YiFgp/CdOvMC5c1LHXGg+RMgDmKqRJVlZZgbu8UDPnsYtMneu2fEHKrtVPB6zwUNNiTy1R6EYH3hIhe1I2rc3/vCPPzY+8NqYPNm0WXXzibr89IFIjx7PEhWVSHb2dEpKTthtjtBCECEPcCwBnDTJ+Jo9HiO427ebRcWiIrj22uqz4OnTvSJaNZGn9igUc80PfmDuczjMlnFjx5pZurVzUFVqC38cN870ESix4/XB4Qinf/9VgIf8/M12myO0EETIgwDLxbJ2rZltWzVMXC7jw/7sMyPUTqcR+Q0bzL6d991XcyJPbVEoU6eaeijp6d7FUjD3zp1rXtdUHsByq0D1L5RAix2vD5GRfUhJOUKHDnfZbYrQQhAhDwIqulhSU70i6XSa8ETw1hq0fN2xsbW7UKw9PqtGobz8shHkdetgzJjKW7hlZNRe66WluFUqEhISC8DZs+8C++w1Rgh6RMiDBEsM58/31kapGPPtcBh3icNhRPXEidpdKOHhVErkqRiFYj0//bS3H4/HiPTEidWTkVqSW6UqHk8JX375KLBAUviFJkWEPIhYtcrMutevh2uuqXzO44HoaMr30HS7a3ehvPMOlRJ5qs6YV60yPvH16839Lpf5gnj9de/CqyXgLc2tUhHjL38LOEdOzgxMOWlB8D0i5EHE9dcbcQTIz698zuGAggLva+u5pkSe1FQqJfLU1M8775jX+/aZdtxu6N/fHLvjDiPg0DLdKhWJiRkGPMDZs2s5deo1u80RghQR8iCiYgTL/PnexUjwzsQBYmLMOY/HJA/t2OFdxKyPsFbs54EHvO6Z7du9ESx9+sATT8QDLdOtUpnJtGlzCwcP/lxCEoUmQYQ8yLAiWObPN2J59dWVzysFeXlmlpySAgkJ8MQTZob8+OPm2UrTr2u3oYqRMlaNcTARLHffbXzlSUnny7NEK7pVUlOD361SGQd9+y6nd+8/ERZ2zeUvF4QG0ighV0otVkplK6V2K6XeUUpd5SvDhCvDimD5yU/g+9+Hb7/1ulKsMrRgRH7HDuMXDwkxQrx2rZkt5+ZWjkCpSdQrRspMnOjdIMLp9PrKjx2LpGdP47oBU0YgIgLmzKFy80cAAB1JSURBVPG20VIID+9Mp073oJSitPSM3eYIQUZjZ+QfAgO11glADvBk400SGsvs2ca18d57RjjT082suaKIh4R4XR0OB4wcCcuWQb9+5nnECHjqqbpF3RLiO+4w/Vi+co/HRL+kp3dg61bzBVJSAt/7nrf/luNWqUx+/mY++yxOUvgFn9IoIddab9BaX6pnx2dAl8abJPgCK2nHmg2vWGHEtm9fkwRUMQbc7TbFtuLjTXEsqxxufUT9l780Ql0x7LG01Oy9Cap8I4r4eNPH9OmmLkzLcatUJjp6CJGRfSSFX/ApvvSRPwD83YftCY1g/XqTuJOaavzZkyebY//7v7BxI7RubdwoaWlGeENDzSYP8fHe2ub1EfUOHcxM++mnYeBA79ZzVnij2212LtqzB5KSYPlyc74luVUqYqXwezzF7N9/L1q7L3+TIFwGdbnYVqVUOtCphlNztNb/d+maOUAycKeupUGl1AxgBkDHjh2TVgXx/9aFhYVERUXZbUaNrFzZla+/bsWYMcZP+/TT/Rk16iz/+Ecnrr++gOzsWHr1KiAnJ5rOnYs5ebIVnTpd5NQp6zmC3r0LOHgwmr59v+PQoSh69izg6NEobr75NO+9dw0hIR7KypwopVHKg8fjJDq6lIKCUMaP/5pOnYqZMuXYZSwNHmr+ffgH8BzwU+Ce5jfKBvz576I5acw4pKambtNaJ1c7obVu1AOYBmwGIut7T1JSkg5mMjIy7DahXjz3nNYffWSen39e63bttH7oIa1bt9Z6/HiT1J+UZJ7j4io/x8eb59BQrZXSOiXFvHY4zHGltAaP9hYHMG22a2f6bEnU9Pvg8Xj0vn1364MHH21+g2wiUP4umprGjAOwVdegqY2NWrkNeAIYr7W+0Ji2hOZn9mxv5qYVgdK9u/F3b9pkFkizs737giYled0ue/YYN4zLZaoibtliXns8EBnpXVi16N0b/v73wN7SzZcopejXbzk9ey6x2xQhCGisj/xFIBr4UCm1Uyn1kg9sEmzgSkR9zx7jE7eiVcBEpFwo/0o3O1C0aQM5OWaRNRi2dPMVSpmYzfz8zzh06BFJ4ReumMZGrfTUWnfVWideeszylWGCfdRX1B96yGwoYelPaGjVzZnNifPnzRfApk3BsaWbr8nP/zfHj/+Okyf/bLcpQoAimZ1CndQm6u+/XzkR6Pnnq4cUWlvCKQX//re4VWqja9fHadPmFg4d+m+KivbbbY4QgIRc/hJBMFguEat2yqJFZrMJa2egr782/vKOHU14Y3a2EXq324QvilulZpQyKfxbtyaQlTWFIUM+w+mMsNssIYCQGblwxcyebaokVoxV/8c/4OGH4cABGD/+a376UxOv/vnnlff6FCoTHt6Zvn3/QlHRLk6e/JPd5ggBhvxpCT6h4kx78WKzw9CQIQcZPfpawGz0nJ4Ojzxik4EBQNu2Y0lI2ECbNmPsNkUIMETIBZ9jlQXIzPQee+QREfH6cPXVJjW2pOQU4CE8XKolCpdHXCuC4Gd4PC527BjB/v1TJYVfqBci5ILgZzgcoVx33Vzy8jI5evRZu80RAgARckHwQzp1mkaHDlM4cmQe+fmb7DZH8HNEyAXBD1FK0bv3MiIiupGVdTcuV57dJgl+jAi5IPgpISGx9O+/8tIGzpK+L9SORK0Igh8TEzOMAQNW222G4OfIjFwQAoCLF3PZsWO0pPALNSJCLggBgMMRzoUL+8jKmozbXWy3OYKfIUIuCAGAN4V/N4cPP263OYKfIUIuCAFC27Zj6dLlYU6ceJFz596z2xzBjxAhF4QAokePZ4mKGsyxY0tkIwqhHIlaEYQAwuEIZ+DA/yM0tC3KKvgutHhkRi4IAUZERFeczkjKygo5d26d3eYIfoAIuSAEKLm5/x97994hKfyC/7hWXC4Xx48fp7g48EOrYmNj2b9f4n2joqJwuVyEhobabUpQ0r37U5w79zeysu4mOXknoaFX2W2SYBN+I+THjx8nOjqa7t27B7zvr6CggOjoaLvNsBWtNcePH+f48ePExcXZbU5QYqXw79jxPXJyZtC//+qA/9sRrgy/ca0UFxfTtq0s4AQLSiliY2OD4j8sfyYmZhjduy/g7Nm1nDr1mt3mCDbhNzNyQEQ8yJCfZ/PQrdtsXK5zXHWVbBHXUvErIRcEoeEo5aBnzyWAcWlpXYbDIesSLYlGuVaUUguUUruVUjuVUhuUUs2zweCiRZCRUflYRoY53sSMHTuWvLy6a0P/5je/IT09/Yraz8zMZNy4cVd0r9Cy8XjK2Lv3Dr788lG7TRGamcb6yBdrrRO01onAOmCuD2y6PEOHwqRJXjHPyDDvhw5tsi611ng8HtavX89VV9UdHfDrX/+aW265pclsEYSacDhCaNWqBydOvMC5c+/bbY7QjDRKyLXW31V425rmqn6fmgpr1hjxnjvXPK9ZY443gqVLlzJw4EAGDhzI73//e3Jzc+nXrx//8R//wZAhQzh27Bjdu3fn3LlzACxYsIC+ffuSlpbGlClTWLLE/Hs7a9Ys3n77bQC6d+/OvHnzGDJkCPHx8WRnZwPwxRdfMGLECAYPHsyIESM4cOBAo2wXBLBS+BPJzp5OSckJu80RmolG+8iVUs8A9wH5QK1KqpSaAcwA6NixI5mZmZXOx8bGUlBQUP+Ok5MJe+ABwhcsoGT2bEqTk6Eh91dhx44d/PnPf+Zf//oXWmvGjBlDcnIyBw4c4MUXX+S5554DzMy8sLCQffv2sXbtWjZu3EhZWRkjR45k4MCBFBQUoLXm4sWL5a+joqL497//zSuvvMLChQt58cUXufbaa/nggw8ICQkhIyOD2bNn89e//pULFy5QVlbWsLHwU9xuN8XFxdV+1i2NwsLCZh6DXwAz2bx5HLAEcDZj37XT/OPgnzTFOFxWyJVS6UCnGk7N0Vr/n9Z6DjBHKfUk8J/AvJra0Vr/CfgTQHJysh49enSl8/v3729Y7HVGBrz2Gjz1FOHLlhF+222NmpHv2LGDH//4x3TqZD7qXXfdxfbt27nuuuu4+eaby69TShEVFcWOHTuYMGECHTp0AOBHP/oR4eHhREdHo5SiVatW5a/vvvtuoqOjufHGG1m/fj3R0dHk5eXxwAMPcPDgQZRSuFwuoqOjiYyMJCQkJCji0AsKCoiIiGDw4MF2m2IrmZmZVP19b2pOnvRw5MiTJCZeR2Rkz2btuzbsGAd/pCnG4bJCrrWur7P3LeADahFyn2L5xC13Smpqo90rtVWSa926dYOur4nw8HAAnE4nZWVlADz11FOkpqbyzjvvkJubK7/ggk/p1Gka7dtPICQk1m5ThGagsVErvSq8HQ9kN86cerJlS2XRtnzmW7ZccZM33XQT7777LhcuXKCoqIh33nmHkSNH1nr99773Pd5//32Ki4spLCzkgw8+aFB/+fn5XHvttQC88cYbV2y3INSEUoqQkFg8njKOHl2My1V3pJUQ2DTWR/6sUqoP4AG+AmY13qR6MHt29WPWzPwKGTJkCPfffz833HADAA8++CBt2rSp9fqhQ4cyfvx4Bg0axHXXXUdycjKxsfWf/cyePZtp06axdOlSxoyRRA6habhwYR9HjvyKgoItksIfzJgEguZ9JCUl6apkZWVVO+bvFBQUaK21Lioq0klJSXrbtm1aa62/++47O83yG7777ruA/Ln6moyMDFv7z81dqDMy0CdOvGKrHXaPg7/QmHEAtuoaNFUyOxvBjBkzyMrKori4mGnTpjFkyBC7TRKEanTrNpvz59M5dOi/iI29kdat+9ltkuBjRMgbwVtvvWW3CYJwWZRy0K/fm2zdmkB29n0MGfKFuFiCDBFyQWgBhId3pn//VTidsSLiQYgIuSC0ENq08eZDlJaeJSysvY3WCL7Eb+qRC4LQPBw79jxffNFPUviDCBFyQWhhtG07Do/nIvv334PWbrvNEXxAQAq5jVVsAfj4448ZMGAAiYmJ7N+/n4EDB15RO2+88QZff/11g+7Jzc294v4EASAysg+9er1IXl4mR48+a7c5gg8ISCFvjiq2+lLZ2ppYsWIFjz32GDt37qRVq1ZX3MeVCLkg+IJOne6nQ4cpHDkyj/z8TXabIzSSgBTyJqpiW61s7Ztvvsnw4cMZMmQIEydOpLCwkFdffZU1a9Ywf/58pk6dWul+t9vN448/zqhRo0hISODll18uP7do0SLi4+MZNGgQv/zlL3n77bfZunUrU6dOJTExkYsXL7Jt2zZGjRpFUlISt956KydPngRg27ZtDBo0iOHDh/PHP/6xcR9SEDAp/L17L6N16/7iKw8GasoSauqHrzI7n3pKazDPvuDIkSNaKaU3b96sz549q0eOHKkLCwu11lo/++yz+umnn9Zaaz1t2jS9du3a8nsGDBigtdb65Zdf1gsWLNDfffedLi4u1klJSfrw4cN6/fr1evjw4bqoqEhrrfU333yjtdZ61KhResuWLVprrUtLS/Xw4cP1mTNntNZar1q1Sk+fPl1rrXV8fLzOzMzUWmv92GOPlffn70hmp8GfMxo9nrJm68ufx6E5kczOCmRkwLJl8NRT5rmRpVbKue6660hJSWHdunVkZWVx4403AlBaWsrw4cPrvHfDhg3s3r2bNWvW4HA4yM/P5+DBg6SnpzN9+nQiIyMBuPrqq6vde+DAAfbu3UtaWhpgZvedO3cmPz+fvLw8Ro0aBcC9997L3//+98Z/UEEAlHKitebUqddQKoROnabZbZJwBQSkkDdBFdtyrLK1WmvS0tJYuXJlve/VWvPCCy8wYsSISvXE//GPf1w2CUNrzYABA9i8eXOl43l5eZLAITQxmjNnVpGf/ynR0TdICn8AEpA+8iaoYluNlJQUPv30Uw4dOgTAhQsXyMnJqfOeW2+9lWXLluFyuQDIycmhqKiI73//+7z22mtcuHABgG+//RaA6Ojo8p2A+vTpw9mzZ8uF3OVysW/fPq666ipiY2P55JNPALPQKgi+RCkHffsux+lsTVbWZNzuYrtNEhpIQAr57NnVZ96pqTVXt71S2rdvzxtvvMGUKVNISEggJSWlfL/N2njwwQfp379/+bZvM2fOpKysjNtuu43x48eTnJxMYmJi+d6e999/P7NmzSIxMRG3283bb7/NE088waBBg0hMTGTTJhNN8Prrr/Ozn/2M4cOHNypKRhBqIzy8M337/oWiot0cPuzDPyShWVC6ATvd+Irk5GS9devWSsf2799Pv37B8S9dQUFBUGzV1lgKCgo4fvx40Pxcr5RA2uLs0KFfcPz470lO3kNUlG/zFQJpHJqSxoyDUmqb1jq56vGA9JELgtA09OjxLFdffbvPRVxoWgLStSIIQtPgcIRz9dVmm97Cwj2Swh8giJALglCNwsK9bNs2RFL4AwQRckEQqtG69QDat58oKfwBggi5IAjVsFL4IyK6kZV1Ny5Xnt0mCXUgQi4IQo2EhMTSv/9KSktPkJMzAzsi3IT6EbBCvuL0abpv3owjM5Pumzez4vTpZrfhwQcfJCsrq85r3n333cteIwj+SkzMMOLifkvr1vGACLm/EpDhhytOn2bGgQNcuFRm9quSEmYcOADA1I4dm82OV1999bLXvPvuu4wbN47+/fs3g0WC4Hu6dXvcbhOEyxCQM/I5hw+Xi7jFBY+HOYcPN6rd3Nxc+vbty7Rp00hISOCuu+7iwoUL/Otf/2Lw4MHEx8fzwAMPUFJSAsDo0aOxEpuioqKYM2cOgwYNYsyYMZw+fZpNmzbx3nvv8fjjj5OYmMiXX37JH/7wB/r3709CQgKTJ09ulL2C0Jx8+206u3bdKin8fohPhFwp9ZhSSiul2vmivctx9JKQ1vd4Qzhw4AAzZsxg9+7dxMTEsHTpUu6//35Wr17Nnj17KCsrY9myZdXuKyoqIiUlhV27dnHjjTfyyiuvMGLECMaPH8/ixYvZuXMn119/Pc8++yw7duxg9+7dvPTSS422VxCaC61LOX9+g6Tw+yGNFnKlVFcgDTjaeHPqR7fw8AYdbwhdu3YtL117zz338K9//Yu4uDh69+4NwLRp09i4cWO1+8LCwhg3bhwAiYmJ5Obm1th+QkICU6dO5a9//SshIQHp2RJaKG3bjqVLl4c5ceIFzp17325zhAr4Ykb+O2A2zbgS8kyPHkQ6Kpse6XDwTI8ejW77SkvGhoaGlt/rdDopKyur8boPPviAn/3sZ2zbto2kpKRarxMEf6RHj2eJikokO3u67CzkRzRqSqiUGg+c0FrvupwAKqVmADMAOnbsSGZmZqXzsbGx5SVdL8f4yEiKu3Xj6a+/5nhpKV3Cwph3zTWMj4ysdxs1UVhYyNGjR0lPT2fYsGEsX76ckSNH8vrrr5e7Rl577TWGDRtGQUEBbreboqKi8j6tZ4/Hg8vloqCggPDwcM6ePUtBQQEej4djx46RnJzMoEGDWLFiBSdPnuSqq666Ypv9GbfbTXFxcbWfdUujsLAwyMbgF8BMNm/+NVD/jSiCbxyujCYZh5q2Dar4ANKBvTU8fgR8DsReui4XaHe59rQPt3rzNUeOHNH9+vXTM2fO1PHx8frOO+/URUVFOj09XScmJuqBAwfq6dOn6+LiYq115a3aWrduXd7O8uXL9bRp07TWWn/yySe6X79+OjExUWdnZ+sbb7xRDxw4UA8YMEAvXLiw2T9jcyJbvRmCcYuzoqJs7fF4GnRPMI7DlWDLVm9a61tqOq6UigfiAGs23gXYrpS6QWt9qnFfL/bhcDiqLULefPPN7Nixo9q1Fb9VCwsLy1/fcccd3HvvvQDceOONleLIrQ0iBCGQiYzsA8DFi19SVpZHdHSSzRa1bK7YtaK13gN0sN4rpXKBZK31OR/YJQiCn6O1Zt++u3C5zpOcvJPQ0OB0EQYCARlH3lR0796dvXv32m2GIAQEph7LS5LC7wf4TMi11t1lNi4ILYuYmGF0776As2fXcurUa3ab02KRGbkgCI2iW7fZXHXVzRw8+HOKiure11ZoGkTIBUFoFEo56NdvOddcM5OIiK52m9MikdRCQRAaTXj4NfTs+TsAPB4XDkeozRa1LGRGXoGoqCgAvv76a+66664m7Ss7O5vExEQGDx7Ml19+6ZM2LPsbStVSu3PnziU9Pf2K2hJaNsXFX7F1a4Kk8DczIuQ1cM011/D22283aR/vvvsuP/rRj9ixYwfXX399rde53bVvflvfNupjS0Uhnz9/PrfcUmP6gCDUSVhYJxyOCEnhb2b8Vsh37Bhd7XHixP8C4HZfqPH8yZNvAFBaeq7auYaQm5vLwIEDAXjjjTe48847ue222+jVqxezZ3srv23YsIHhw4czZMgQJk6cWCkpyGLnzp2kpKSQkJDAhAkTOH/+POvXr+f3v/89r776KqmpqdXuiYqKYu7cuQwbNozNmzezbds2Ro0aRVJSErfeeisnT568bBuLFy9m6NChJCQkMG/evPLjy5cvJyEhgUGDBnHvvffWWGr3/vvvL/8iq62Eb/fu3Zk3bx5DhgwhPj6e7GxZ5BLA4Qinf/9VeDwX2b//XrSufSIi+A6/FXJ/YufOneVlbFevXs2xY8c4d+4cv/nNb0hPT2f79u0kJyezdOnSavfed999PPfcc+zevZv4+Hiefvppxo4dy6xZs/jFL35BRkZGtXuKiooYOHAgn3/+OcOGDePnP/85b7/9Ntu2beOBBx5gzpw5dbaxYcMGDh48yBdffMHOnTvZtm0bGzduZN++fTzzzDN89NFH7Nq1i//5n/+psdSuRXFxcZ0lfNu1a8f27dt56KGHWLJkiQ9HXAhkIiP70KvXi+TlZXD06LN2m9Mi8NvFzsGDM2s953RG1nk+LKxdnecbys0330xsbCwA/fv356uvviIvL4+srKzykrelpaUMHz680n35+fnk5eUxatQowJTAnThx4mX7czqd/PjHPwZMffS9e/eSlpYGGFdL586d67x/w4YNbNiwgcGDBwOmfMDBgwfZtWsXd911F+3ambLxV199dZ3tHDhwoFoJ3z/+8Y88/PDDANx5550AJCUl8be//e2yn0toOXTqdD/nz2/gm2/+TteuT+Bw+K3UBAUyuvUgvEKdc6tErdaatLQ0Vq5c6fP+IiIicDqdgEmDHjBgAJs3b673/VprnnzySWbOnFnp+B/+8IcGlem9XKaeNS51le0VWiYm6/MVHI5wVp79hjmHD3MU6LZ5M8/06NGsWzK2BMS1coWkpKTw6aefcujQIQAuXLhATk5OpWtiY2Np06YNH3/8MQBvvvlm+ey8vvTp04ezZ8+WC7nL5WLfvn113nPrrbfy2muvlfvsT5w4wZkzZ7j55ptZs2YN33zzDQDffvstANHR0TWW/+3bty+5ubnln/FK7BdaLiEhUaw8+y2PZn/BDSVvotHl++vasVl6MCNCfoW0b9+eN954gylTppCQkEBKSkqNC35/+ctfePzxx0lISGDnzp3MnTu3Qf2EhYXx9ttv88QTTzBo0CASExPZtGlTnfd8//vf5+6772b48OHEx8dz1113UVBQwIABA5gzZw6jRo1i0KBBPPLIIwBMnjyZxYsXVwuFjIiI4PXXX2fixInEx8fjcDiYNWtWg+wXWjZzDh/me/qfTOd1rsVEsfhif12hMsqOQjfJycna2rTYYv/+/fTr16/ZbWkKCgoKiI6OttsM2ykoKOD48eNB83O9UjIzMxk9erTdZtiCIzMT8NCR05zCu7ajAE8LHZPG/D4opbZprZOrHpcZuSAITUa38HA0jkoibh0XfIcIuSAITUZT7q8rePErIZd6xsGF/DyFqR078qc+fbguPBwFXBcezp/69JGoFR/jN0IeERHBN998I3/8QYLWmvz8fCIiIuw2RbCZqR07kjt8OB8BucOHi4g3AX4TR96lSxeOHz/O2bNn7Tal0RQXF4uAYTJUBw0aZLcZghD0+I2Qh4aGEhcXZ7cZPiEzM7M8q7Ilk5mZSWiolDMVhKbGb1wrgiAIwpUhQi4IghDgiJALgiAEOLZkdiqlzgJfNXvHzUc74JzdRvgBMg4GGQeDjIOhMeNwnda6fdWDtgh5sKOU2lpTGm1LQ8bBIONgkHEwNMU4iGtFEAQhwBEhFwRBCHBEyJuGP9ltgJ8g42CQcTDIOBh8Pg7iIxcEQQhwZEYuCIIQ4IiQC4IgBDgi5E2MUuoxpZRWSrWz2xY7UEotVkplK6V2K6XeUUpdZbdNzYVS6jal1AGl1CGl1C/ttscOlFJdlVIZSqn9Sql9Sqn/ttsmO1FKOZVSO5RS63zZrgh5E6KU6gqkAUfttsVGPgQGaq0TgBzgSZvtaRaUUk7gj8APgP7AFKVUf3utsoUy4FGtdT8gBfhZCx0Hi/8G9vu6URHypuV3wGygxa4oa603aK3LLr39DOhipz3NyA3AIa31Ya11KbAK+JHNNjU7WuuTWuvtl14XYETsWnutsgelVBfgduBVX7ctQt5EKKXGAye01rvstsWPeAD4u91GNBPXAscqvD9OCxUwC6VUd2Aw8Lm9ltjG7zETO4+vG/abeuSBiFIqHehUw6k5wK+A7zevRfZQ1zhorf/v0jVzMP9mr2hO22xE1XCsxf5nppSKAv5/4GGt9Xd229PcKKXGAWe01tuUUqN93b4IeSPQWt9S03GlVDwQB+xSSoFxJ2xXSt2gtT7VjCY2C7WNg4VSahowDrhZt5zEheNA1wrvuwBf22SLrSilQjEivkJr/Te77bGJG4HxSqmxQAQQo5T6q9b6Hl80LglBzYBSKhdI1lq3uMpvSqnbgKXAKK114O/jV0+UUiGYxd2bgRPAFuBurfU+Ww1rZpSZyfwF+FZr/bDd9vgDl2bkj2mtx/mqTfGRC03Ni0A08KFSaqdS6iW7DWoOLi3w/ifwT8wC35qWJuKXuBG4Fxhz6ee/89KsVPAhMiMXBEEIcGRGLgiCEOCIkAuCIAQ4IuSCIAgBjgi5IAhCgCNCLgiCEOCIkAuCIAQ4IuSCIAgBzv8Dyq5wU0nPEK4AAAAASUVORK5CYII=\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(reflection_normal=[1,0], distance_to_origin=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` 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:" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [], + "source": [ + "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", + "\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": "markdown", + "metadata": {}, + "source": [ + "Now we generate a shape, add an instance of this segment and rasterize it." + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(-0.7939352559383557,\n", + " 10.513996916949445,\n", + " -5.513996916949446,\n", + " 5.793935255938355)" + ] + }, + "execution_count": 20, + "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": 21, + "metadata": {}, + "outputs": [], + "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": 22, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 22, + "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, 2)\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], '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", + "plt.axis(\"equal\")\n", + "plt.legend(loc=\"lower right\")" + ] + } + ], + "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 +}