turbo_turtle._abaqus_python.turbo_turtle_abaqus.vertices — turbo-turtle 1.2.9 documentation

Skip to main content
`Ctrl`+`K`

#

# Source code for turbo_turtle._abaqus_python.turbo_turtle_abaqus.vertices

    """Provide Python 2/3 compatible coordinate handling for Abaqus Python scripts and Turbo-Turtle Python 3 modules."""

    import cmath

    import numpy

    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

    def cylinder(inner_radius, outer_radius, height, y_offset=0.0):
        """Return :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
        """  # noqa: E501
        coordinates = (
            (inner_radius, height / 2.0 + y_offset),
            (outer_radius, height / 2.0 + y_offset),
            (outer_radius, -height / 2.0 + y_offset),
            (inner_radius, -height / 2.0 + y_offset),
        )
        return numpy.array(coordinates)

    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

    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

    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

    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

    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]]
        # Abaqus 2023 Python does not have ``itertools.pairwise``.
        # TODO: Remove RUF007 exception when Abaqus 2024 is the oldest supported Abaqus version.
        for spline1, spline2 in zip(all_splines[0:-1], all_splines[1:]):  # noqa: RUF007
            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

    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

    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

    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)
            or numpy.isclose(coords1[1], coords2[1], **isclose_kwargs)
            for coords1, coords2 in zip(coordinates[1:, :], coordinates[0:-1, :])
        ]
        return vertical_horizontal_bools

    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

    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
        """
        # Abaqus 2023 Python does not have ``itertools.pairwise``.
        # TODO: Remove RUF007 exception when Abaqus 2024 is the oldest supported Abaqus version.
        zipped_splines = zip(all_splines[0:-1], all_splines[1:])  # noqa: RUF007
        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

    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

    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

    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)

    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

    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),
            midpoint_vector(xvector, -yvector),
            midpoint_vector(yvector, zvector),
            midpoint_vector(yvector, -zvector),
            midpoint_vector(zvector, xvector),
            midpoint_vector(zvector, -xvector),
        ]
        midpoints = [normalize_vector(midpoint) for midpoint in midpoints]

        return primary_planes + midpoints

    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
            midpoint_vector(-xvector, yvector, zvector),  # 1
            midpoint_vector(-xvector, yvector, -zvector),  # 2
            midpoint_vector(xvector, yvector, -zvector),  # 3
            midpoint_vector(xvector, -yvector, zvector),  # 4
            midpoint_vector(-xvector, -yvector, zvector),  # 5
            midpoint_vector(-xvector, -yvector, -zvector),  # 6
            midpoint_vector(xvector, -yvector, -zvector),  # 7
        ]
        fortyfives = [normalize_vector(vector) for vector in fortyfives]

        return fortyfives

    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