Source code for turbo_turtle._abaqus_python.turbo_turtle_abaqus.vertices

"""Python 2/3 compatible coordinate handling for use in both Abaqus Python scripts and Turbo-Turtle Python 3 modules"""

import math
import cmath

import numpy


[docs] def rectalinear_coordinates(radius_list, angle_list): """Calculate 2D rectalinear XY coordinates from 2D polar coordinates :param list radius: length N list of polar coordinate radius :param list angle: length N list of polar coordinate angle measured from the positive X-axis in radians :returns coords: length N tuple of tuple(X, Y) rectalinear coordinates :rtype: list """ numbers = (cmath.rect(radius, angle) for radius, angle in zip(radius_list, angle_list)) coordinates = tuple((number.real, number.imag) for number in numbers) return coordinates
[docs] def cylinder(inner_radius, outer_radius, height, y_offset=0.0): """Return a :meth:`turbo_turtle._abaqus_python.turbo_turtle_abaqus.vertices.lines_and_splines` compatible vertex array :param float inner_radius: Radius of the hollow center :param float outer_radius: Outer radius of the cylinder :param float height: Height of the cylinder :returns: vertex coordinate array :rtype: numpy.array """ coordinates = ( (inner_radius, height / 2.0 + y_offset), # fmt: skip # noqa: E241 (outer_radius, height / 2.0 + y_offset), # fmt: skip # noqa: E241 (outer_radius, -height / 2.0 + y_offset), (inner_radius, -height / 2.0 + y_offset), ) return numpy.array(coordinates)
[docs] def cylinder_lines(inner_radius, outer_radius, height, y_offset=0.0): """Return the line coordinate pairs defining a cylinder :param float inner_radius: Radius of the hollow center :param float outer_radius: Outer radius of the cylinder :param float height: Height of the cylinder :returns: list of line segment coordinate pairs :rtype: list of (numpy.array, numpy.array) tuples """ coordinates = cylinder(inner_radius, outer_radius, height, y_offset=y_offset) euclidean_distance = min(inner_radius, height) / 2.0 lines, splines = lines_and_splines(coordinates, euclidean_distance) return lines
[docs] def sphere(center, inner_radius, outer_radius, quadrant): """Return the inner and outer radii vertices defining a 2D sketh of a sphere for revolution :param tuple center: tuple of floats (X, Y) location for the center of the sphere :param float inner_radius: inner radius (size of hollow) :param float outer_radius: outer radius (size of sphere) :param str quadrant: quadrant of XY plane for the sketch: upper (I), lower (IV), both :returns: [4, 2] inner/outer radii in (x, y) coordinates defining inner/outer circular arc segments :rtype: numpy.array """ inner_radius = abs(inner_radius) outer_radius = abs(outer_radius) if quadrant == "both": start_angle = -numpy.pi / 2.0 end_angle = numpy.pi / 2.0 elif quadrant == "upper": start_angle = 0.0 end_angle = numpy.pi / 2.0 elif quadrant == "lower": start_angle = -numpy.pi / 2.0 end_angle = 0.0 radius_list = (inner_radius, inner_radius, outer_radius, outer_radius) angle_list = (end_angle, start_angle, end_angle, start_angle) points = numpy.array(center) + numpy.array(rectalinear_coordinates(radius_list, angle_list)) return points
[docs] def scale_and_offset_coordinates(coordinates, unit_conversion=1.0, y_offset=0.0): """Scale and offset XY coordinates in a 2 column numpy array First multiply by the unit conversion. Then offset the Y coordinates (2nd column) by adding the y offset :param numpy.array coordinates: [N, 2] array of XY coordinates. :param float unit_conversion: multiplication factor applies to all coordinates :param float y_offset: vertical offset along the global Y-axis. Offset should be provided in units *after* the unit conversion. """ coordinates = coordinates * unit_conversion coordinates[:, 1] += y_offset return coordinates
[docs] def lines_and_splines(coordinates, euclidean_distance, rtol=None, atol=None): """Accept a [N, 2] numpy array of XY coordinates and return line point pairs and splines Array is broken into a list of [M, 2] arrays according to the following rules #. If neighboring points are farther apart than the euclidean distance, break the original array between them. #. If neighboring points have the same X or Y coordinate (horizontally or vertically aligned), break the original array between them. Uses ``numpy.isclose`` with the default tolerance for float comparison. After breaking into a list of arrays, the line pair list and spline list are generated from the following rules #. Line point pairs are returned for the end and beginning of adjacent arrays, and for the end of the last array and the beginning of the first array. #. Arrays of length 2 are converted to line pair coordinates #. Arrays greater than length 2 are kept intact as splines. :param numpy.array coordinates: [N, 2] array of XY coordinates. :param float euclidean_distance: If the distance between two points is greater than this, draw a straight line. :param float rtol: relative tolerance used by ``numpy.isclose``. If None, use the numpy default. :param float atol: absolute tolerance used by ``numpy.isclose``. If None, use the numpy default. :returns: list of line pairs and list of spline arrays :rtype: tuple """ all_splines = _break_coordinates(coordinates, euclidean_distance, rtol=rtol, atol=atol) lines = _line_pairs(all_splines) lines.extend([numpy.stack((array[0], array[1])) for array in all_splines if len(array) == 2]) splines = [array for array in all_splines if len(array) > 2] return lines, splines
[docs] def ordered_lines_and_splines(coordinates, euclidean_distance, rtol=None, atol=None): """Return a single, closed loop list of [M, 2] arrays with lines (length 2) and splines (length >2)""" all_splines = _break_coordinates(coordinates, euclidean_distance, rtol=rtol, atol=atol) lines_and_splines = [all_splines[0]] for spline1, spline2 in zip(all_splines[0:-1], all_splines[1:]): lines_and_splines.append(numpy.stack((spline1[-1], spline2[0]))) lines_and_splines.append(spline2) lines_and_splines.append(numpy.stack((all_splines[-1][-1], all_splines[0][0]))) # Eliminate points after creating line connections lines_and_splines = [array for array in lines_and_splines if len(array) > 1] return lines_and_splines
[docs] def _break_coordinates(coordinates, euclidean_distance, rtol=None, atol=None): """Accept a [N, 2] numpy array and break into a list of [M, 2] arrays This function follows this methodology to turn a [N, 2] numpy array into a list of [M, 2] arrays denoting individual lines or splines. #. If neighboring points are farther apart than the euclidean distance, break the original array between them. #. If neighboring points have the same X or Y coordinate (horizontally or vertically aligned), break the original array between them. Uses ``numpy.isclose`` with the default tolerance for float comparison. :param numpy.array coordinates: [N, 2] array of XY coordinates. :param float euclidean_distance: If the distance between two points is greater than this, draw a straight line. :param float rtol: relative tolerance used by ``numpy.isclose``. If None, use the numpy default. :param float atol: absolute tolerance used by ``numpy.isclose``. If None, use the numpy default. :return: Series of line and spline definitions :rtype: list """ euclidean_distance_bools = _compare_euclidean_distance(coordinates, euclidean_distance) vertical_horizontal_bools = _compare_xy_values(coordinates, rtol=rtol, atol=atol) bools_from_or = _bool_via_or(euclidean_distance_bools, vertical_horizontal_bools) break_indices = numpy.where(bools_from_or)[0] all_splines = numpy.split(coordinates, break_indices, axis=0) return all_splines
[docs] def _compare_euclidean_distance(coordinates, euclidean_distance): """Compare the distance between coordinates in a 2D numpy array of XY data to a provided euclidean distance The distance comparison is performed as ``numpy_array_distance > euclidean_distance``. The distance between coordinates in the numpy array is computed such that the "current point" is compared to the previous point in the list. As such, a single ``False`` is always prepended to the beginning of the output ``euclidean_distance_bools`` list, because there is no such distance between the first point and one that comes before it. :param numpy.array coordinates: [N, 2] array of XY coordinates. :param float euclidean_distance: distance value to compare against :return: bools for the distance comparison :rtype: list of length N """ calculated_euclidean_array = numpy.linalg.norm(coordinates[1:, :] - coordinates[0:-1, :], axis=1) euclidean_distance_bools = [False] + [ this_euclidean_distance > euclidean_distance for this_euclidean_distance in calculated_euclidean_array ] return euclidean_distance_bools
[docs] def _compare_xy_values(coordinates, rtol=None, atol=None): """Check neighboring XY values in an [N, 2] array of coordinates for vertical or horizontal relationships This function loops through lists of coordinates checking to see if a "current point" and the previous point in the numpy array are vertical or hozitonal from one another. As such, a single ``False`` is always prepended to the beginning of the output ``vertical_horizontal_bools`` list, because there is no such vertical/horizontal relationship between the first point and one that comes before it. :param numpy.array coordinates: [N, 2] array of XY coordinates. :param float rtol: relative tolerance used by ``numpy.isclose``. If None, use the numpy default. :param float atol: absolute tolerance used by ``numpy.isclose``. If None, use the numpy default. :return: bools for vertical/horizontal relationship comparison :rtype: list of length N """ isclose_kwargs = {} if rtol is not None: isclose_kwargs.update({"rtol": rtol}) if atol is not None: isclose_kwargs.update({"atol": atol}) vertical_horizontal_bools = [False] + [ numpy.isclose(coords1[0], coords2[0], **isclose_kwargs) # noqa: W504 or numpy.isclose(coords1[1], coords2[1], **isclose_kwargs) # noqa: W503 for coords1, coords2 in zip(coordinates[1:, :], coordinates[0:-1, :]) ] return vertical_horizontal_bools
[docs] def _bool_via_or(bools_list_1, bools_list_2): """Compare two lists of bools using an ``or`` statement :param list bools_list_1: first set of bools :param list bools_list_2: second set of bools :return: bools resulting from ``or`` statment :rtype: list """ bools_from_or = [a or b for a, b in zip(bools_list_1, bools_list_2)] return bools_from_or
[docs] def _line_pairs(all_splines): """Accept a list of [N, 2] arrays and return a list of paired coordinates to connect as lines Given a list of [N, 2] numpy arrays, create tuple pairs of coordinates between the end and beginning of subsequent arrays. Also return a pair from the last array's last coordinate to the first array's first coordinate. :param list all_splines: a list of 2D numpy arrays :returns: line pairs :rtype: list of [2, 2] numpy arrays """ zipped_splines = zip(all_splines[0:-1], all_splines[1:]) line_pairs = [numpy.stack((spline1[-1], spline2[0])) for spline1, spline2 in zipped_splines] line_pairs.append((all_splines[-1][-1], all_splines[0][0])) return line_pairs
[docs] def normalize_vector(vector): """Normalize a cartesian vector :param list vector: List of three floats defining a cartesian vector :returns: normalized :rtype: numpy.array """ numpy.array(vector) norm = numpy.linalg.norm(vector) if numpy.isclose(norm, 0.0): return vector return vector / norm
[docs] def midpoint_vector(first, second, third=None): """Calculate the vector between two vectors (summation / 2) :param numpy.array first: First vector :param numpy.array second: Second vector :returns: Vector midway between first and second vector :rtype: numpy.array """ first = numpy.array(first) second = numpy.array(second) summation = first + second midpoint = summation / 2.0 if third is not None: third = numpy.array(third) summation = summation + third midpoint = summation / 3.0 return midpoint
[docs] def is_parallel(first, second, rtol=None, atol=None): """Compute cross product. If it is near zero, return True. Else False. :param numpy.array first: First vector :param numpy.array second: Second vector :returns: boolean answering "are these vectors parallel?" :rtype: bool """ first = numpy.array(first) second = numpy.array(second) kwargs = {} if rtol is not None: kwargs.update({"rtol": rtol}) if atol is not None: kwargs.update({"atol": atol}) return numpy.allclose(numpy.cross(first, second), 0.0, **kwargs)
[docs] def any_parallel(first, options, rtol=None, atol=None): """If the first vector is parellel to any of the options, return True :param numpy.array first: First vector :param list options: List of vectors to compare against the first vector :returns: boolean answering "is the first vector parallel to any of the option vectors?" :rtype: bool """ for second in options: if is_parallel(first, second, rtol=rtol, atol=atol): return True return False
[docs] def datum_planes(xvector, zvector): """Calculate the sphere partitioning datum plane normal vectors on a local coordinate system The x- and z-vectors must be orthogonal. They will be normalized prior to calculating the normalized plane normal vectors. :param list xvector: List of three (3) floats defining the local x-axis vector in global coordinate space :param list zvector: List of three (3) floats defining the local z-axis vector in global coordinate space :returns: list of normalized local plane normal vectors [9, 3] - (3) xy/yz/zx planes, (6) +/- 45 degrees from xy/yz/zx planes :rtype: list """ dot = numpy.dot(xvector, zvector) if not numpy.isclose(dot, 0.0): raise RuntimeError("Provided x-vector '{}' and z-vector '{}' are not orthogonal".format(xvector, zvector)) xvector = normalize_vector(xvector) zvector = normalize_vector(zvector) xy_plane = zvector yz_plane = xvector yvector = numpy.cross(zvector, xvector) zx_plane = yvector primary_planes = [xy_plane, yz_plane, zx_plane] midpoints = [ midpoint_vector(xvector, yvector), # fmt: skip # noqa: E201,E241 midpoint_vector(xvector, -yvector), # fmt: skip # noqa: E201,E241 midpoint_vector(yvector, zvector), # fmt: skip # noqa: E201,E241 midpoint_vector(yvector, -zvector), # fmt: skip # noqa: E201,E241 midpoint_vector(zvector, xvector), # fmt: skip # noqa: E201,E241 midpoint_vector(zvector, -xvector), # fmt: skip # noqa: E201,E241 ] midpoints = [normalize_vector(midpoint) for midpoint in midpoints] return primary_planes + midpoints
[docs] def fortyfive_vectors(xvector, zvector): """Return the normalized (1, 1, 1) vector variants of a local coordinate system defined by the x- and z-vector""" dot = numpy.dot(xvector, zvector) if not numpy.isclose(dot, 0.0): raise RuntimeError("Provided x-vector '{}' and z-vector '{}' are not orthogonal".format(xvector, zvector)) xvector = normalize_vector(xvector) zvector = normalize_vector(zvector) yvector = numpy.cross(zvector, xvector) fortyfives = [ midpoint_vector( xvector, yvector, zvector), # 0 # fmt: skip # noqa: E201,E241 midpoint_vector(-xvector, yvector, zvector), # 1 # fmt: skip # noqa: E201,E241 midpoint_vector(-xvector, yvector, -zvector), # 2 # fmt: skip # noqa: E201,E241 midpoint_vector( xvector, yvector, -zvector), # 3 # fmt: skip # noqa: E201,E241 midpoint_vector( xvector, -yvector, zvector), # 4 # fmt: skip # noqa: E201,E241 midpoint_vector(-xvector, -yvector, zvector), # 5 # fmt: skip # noqa: E201,E241 midpoint_vector(-xvector, -yvector, -zvector), # 6 # fmt: skip # noqa: E201,E241 midpoint_vector( xvector, -yvector, -zvector), # 7 # fmt: skip # noqa: E201,E241 ] fortyfives = [normalize_vector(vector) for vector in fortyfives] return fortyfives
[docs] def pyramid_surfaces(center, xvector, zvector, big_number): """Return the pyramid surfaces defined by the center and vertices of a cube Returns arrays of [N, 2] coordinates defining 12 triangular surfaces and 6 square surfaces defining the 4 edge pyramids from the center of a cube to the cube vertices. :returns: list of numpy arrays, where each numpy array is an [N, 3] list of coordinates defining a surface :rtype: list of numpy.array """ vectors = fortyfive_vectors(xvector, zvector) fortyfive_vertices = [center + vector * big_number for vector in vectors] # TODO: Figure out how to cleanup these coordinate pairs such that they are independent from the fortyfives indices surface_coordinates = [ # +Y surfaces numpy.array([center, fortyfive_vertices[0], fortyfive_vertices[1]]), # 0: +Y +Z numpy.array([center, fortyfive_vertices[1], fortyfive_vertices[2]]), # 1: -X +Y numpy.array([center, fortyfive_vertices[2], fortyfive_vertices[3]]), # 2: +Y -Z numpy.array([center, fortyfive_vertices[3], fortyfive_vertices[0]]), # 3: +X +Y # -Y surfaces numpy.array([center, fortyfive_vertices[4], fortyfive_vertices[5]]), # 4: -Y +Z numpy.array([center, fortyfive_vertices[5], fortyfive_vertices[6]]), # 5: -X -Y numpy.array([center, fortyfive_vertices[6], fortyfive_vertices[7]]), # 6: -Y -Z numpy.array([center, fortyfive_vertices[7], fortyfive_vertices[4]]), # 7: +X -Y # +X surfaces numpy.array([center, fortyfive_vertices[0], fortyfive_vertices[4]]), # 8: +X +Z numpy.array([center, fortyfive_vertices[3], fortyfive_vertices[7]]), # 9: +X -Z # -X surfaces numpy.array([center, fortyfive_vertices[1], fortyfive_vertices[5]]), # 10: -X +Z numpy.array([center, fortyfive_vertices[2], fortyfive_vertices[6]]), # 11: -X -Z # +/- normal to Y numpy.array(fortyfive_vertices[0:4]), # 12: +Y numpy.array(fortyfive_vertices[4:]), # 13: -Y # +/- normal to X numpy.array( [fortyfive_vertices[0], fortyfive_vertices[3], fortyfive_vertices[7], fortyfive_vertices[4]] # 14: +X ), numpy.array( [fortyfive_vertices[1], fortyfive_vertices[2], fortyfive_vertices[6], fortyfive_vertices[5]] # 15: -X ), # +/- normal to Z numpy.array( [fortyfive_vertices[0], fortyfive_vertices[1], fortyfive_vertices[5], fortyfive_vertices[4]] # 16: +Z ), numpy.array( [fortyfive_vertices[2], fortyfive_vertices[3], fortyfive_vertices[7], fortyfive_vertices[6]] # 17: -Z ), ] return surface_coordinates