Coordinates

In structural dynamics, bookkeeping is an incredibly important part of analyzing test or analysis data. Due to the prevelance of linear algebra operations, we often work with matrices where each row might represent a different measurement or analysis degree of freedom. If we mix up which row represents which channel, then the results of our analysis will be incorrect. Therefore, SDynPy labels its data objects with Coordinates, or degrees of freedom.

This document will describe how coordinates are defined and used in SDynPy. We will start by showing how we can define coordinates, and then walk through the different use-cases in SDynPy.

Let’s import SDynPy and start looking at Coordinates!

[1]:
import sdynpy as sdpy
import numpy as np

SDynPy Coordinate Objects

In SDynPy, the object used to represent a coordinate or a degree of freedom in a test or analysis is the CoordinateArray. As the name implies, it is well suited for storing multiple coordinates in an array, which may be all the degrees of freedom from a test or analysis, or some subset thereof.

A CoordinateArray consists of two parts, a node identification number and a direction. When referenced to a Geometry object, these will define the location and direction of measurement or analysis data.

Node identification numbers are generally positive integers corresponding to a node identification number in a NodeArray in a Geometry object.

The direction corresponds to the local coordinate system direction of that node’s displacement coordinate system (recall that nodes have both a definition and displacement coordinate system referenced). The direction can be one of

  • X+ - The positive \(X\) direction

  • X- - The negative \(X\) direction

  • Y+ - The positive \(Y\) direction

  • Y- - The negative \(Y\) direction

  • Z+ - The positive \(Z\) direction

  • Z- - The negative \(Z\) direction

  • RX+ - A rotation about the positive \(X\) direction

  • RX- - A rotation about the negative \(X\) direction

  • RY+ - A rotation about the positive \(Y\) direction

  • RY- - A rotation about the negative \(Y\) direction

  • RZ+ - A rotation about the positive \(Z\) direction

  • RZ- - A rotation about the negative \(Z\) direction

  • (empty) - No direction

The empty direction is often used to represent what might be described as “scalar” degrees of freedom, for example a modal degree of freedom in a modal model, or a fixed-base modal degree of freedom in a Craig-Bampton substructure. This could also be used data from microphones or thermocouples which is not associated with a particular direction.

Creating Coordinates from Scratch

Coordinates are defined with the CoordinateArray object in SDynPy. As the name implies, this is a SdynpyArray subclass, and therefore has all of the capabilities of the SdynpyArray object and NumPy ndarray objects. Similarly to all other SdynpyArray subclasses, it has a function coordinate_array to help us construct CoordinateArray objects that uses the same name except with snake_case capitalization instead of CamelCase.

The coordinate_array function can be used in a few different ways. The first approach is to simply pass a node and a direction.

[2]:
coords = sdpy.coordinate_array(
    node = 10,
    direction = 'Z-')

Let’s briefly explore the fields of the CoordinateArray object and their dtype before continuing.

[3]:
coords.fields
[3]:
('node', 'direction')
[4]:
coords.dtype
[4]:
dtype([('node', '<u8'), ('direction', 'i1')])

We can see that the node information is stored as an unsigned integer (an integer >= 0), and the direction field is also stored as an integer, though only an 8-bit integer. SDynPy encodes direction information as integers with the following scheme:

  • \(X+ = 1\)

  • \(X- = -1\)

  • \(Y+ = 2\)

  • \(Y- = -2\)

  • \(Z+ = 3\)

  • \(Z- = -3\)

  • \(RX+ = 4\)

  • \(RX- = -4\)

  • \(RY+ = 5\)

  • \(RY- = -5\)

  • \(RZ+ = 6\)

  • \(RZ- = -6\)

  • (empty) \(= 0\)

To generate a CoordinateArray with multiple entries in it, we can simply pass multiple entries to the arguments of the coordinate_array function.

[5]:
coords = sdpy.coordinate_array(
    node = [1,1,1,2,2,2],
    direction = ['X+','Y-','Z-','Y+','X-','Z+'])

To see a representation of the CoordinateArray, we can simply type the variable name into the terminal.

[6]:
coords
[6]:
coordinate_array(string_array=
array(['1X+', '1Y-', '1Z-', '2Y+', '2X-', '2Z+'], dtype='<U3'))

The direction argument can also be passed integer values per the encoding. This can be more compact (no strings required, the five typed characters of 'RX+' becomes one character 4), but less explicit.

[7]:
coords = sdpy.coordinate_array(
    node = [1,1,1,2,2,2],
    direction = [1,-2,-3,2,-1,3])
coords
[7]:
coordinate_array(string_array=
array(['1X+', '1Y-', '1Z-', '2Y+', '2X-', '2Z+'], dtype='<U3'))

Note that if the node and direction arguments are not the same sizes, a broadcast will be attempted using the usual NumPy broadcasting rules. For example, if we wanted all ['X+','Y+','Z+'] directions for all the nodes between one and ten, we could do something like this:

[8]:
coords = sdpy.coordinate_array(
    node = (np.arange(10)+1)[:,np.newaxis],
    direction = [1,2,3])
coords
[8]:
coordinate_array(string_array=
array([['1X+', '1Y+', '1Z+'],
       ['2X+', '2Y+', '2Z+'],
       ['3X+', '3Y+', '3Z+'],
       ['4X+', '4Y+', '4Z+'],
       ['5X+', '5Y+', '5Z+'],
       ['6X+', '6Y+', '6Z+'],
       ['7X+', '7Y+', '7Z+'],
       ['8X+', '8Y+', '8Z+'],
       ['9X+', '9Y+', '9Z+'],
       ['10X+', '10Y+', '10Z+']], dtype='<U4'))

In the previous, we use np.arange to create a list of numbers from 0-9 to which we add 1, which makes an array from 1 to 10. Adding the np.newaxis in the second indexing position creates a new axis at that location, transforming the shape (10,) array into a shape (10,1) array. This can then be broadcast with the shape (3,) direction array, with the last dimension of the node array being expanded from length 1 to length 3, and the implicit length-1 dimension of the direction array being expanded to length 10. This then results in a shape (10,3) coordinate array.

[9]:
coords.shape
[9]:
(10, 3)

For a full treatment of NumPy’s broadcasting rules, see the relevant NumPy documentation.

Obviously, if the broadcasted arrays are not compatible, an error will be thrown.

[10]:
coords = sdpy.coordinate_array(
    node = (np.arange(10)+1), # No np.newaxis, so arrays are incompatible
    direction = [1,2,3])
coords
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
File ~\Documents\Local_Repositories\sdynpy\src\sdynpy\core\sdynpy_coordinate.py:364, in coordinate_array(node, direction, structured_array, string_array, force_broadcast)
    363 try:
--> 364     bc_node, bc_direction = np.broadcast_arrays(node, direction)
    365 except ValueError:

File C:\Program Files\Python313\Lib\site-packages\numpy\lib\_stride_tricks_impl.py:544, in broadcast_arrays(subok, *args)
    542 args = [np.array(_m, copy=None, subok=subok) for _m in args]
--> 544 shape = _broadcast_shape(*args)
    546 result = [array if array.shape == shape
    547           else _broadcast_to(array, shape, subok=subok, readonly=False)
    548                           for array in args]

File C:\Program Files\Python313\Lib\site-packages\numpy\lib\_stride_tricks_impl.py:419, in _broadcast_shape(*args)
    417 # use the old-iterator because np.nditer does not handle size 0 arrays
    418 # consistently
--> 419 b = np.broadcast(*args[:32])
    420 # unfortunately, it cannot handle 32 or more arguments directly

ValueError: shape mismatch: objects cannot be broadcast to a single shape.  Mismatch is between arg 0 with shape (10,) and arg 1 with shape (3,).

During handling of the above exception, another exception occurred:

ValueError                                Traceback (most recent call last)
Cell In[10], line 1
----> 1 coords = sdpy.coordinate_array(
      2     node = (np.arange(10)+1), # No np.newaxis, so arrays are incompatible
      3     direction = [1,2,3])
      4 coords

File ~\Documents\Local_Repositories\sdynpy\src\sdynpy\core\sdynpy_coordinate.py:366, in coordinate_array(node, direction, structured_array, string_array, force_broadcast)
    364         bc_node, bc_direction = np.broadcast_arrays(node, direction)
    365     except ValueError:
--> 366         raise ValueError('node and direction should be broadcastable to the same shape (node: {:}, direction: {:})'.format(
    367             node.shape, direction.shape))
    369 # Create the coordinate array
    370 coord_array = CoordinateArray(bc_node.shape)

ValueError: node and direction should be broadcastable to the same shape (node: (10,), direction: (3,))

If the user doesn’t understand or can’t be bothered to learn about broadcasting in NumPy, they can provide the force_broadcast argument to force SDynPy to make it work. Note that all shape information will be lost as the input arrays will be flattened upon input to force the broadcasting to work. The author would however implore the user to reconsider and put forth the effort to learn how to broadcast arrays, as it can lead to very elegant handling of multidimensional arrays.

[11]:
coords = sdpy.coordinate_array(
    node = (np.arange(10)+1), # No np.newaxis, so arrays are incompatible
    direction = [1,2,3],
    force_broadcast=True) # Tell SDynPy you don't care if the arrays are incompatible
coords
[11]:
coordinate_array(string_array=
array(['1X+', '1Y+', '1Z+', '2X+', '2Y+', '2Z+', '3X+', '3Y+', '3Z+',
       '4X+', '4Y+', '4Z+', '5X+', '5Y+', '5Z+', '6X+', '6Y+', '6Z+',
       '7X+', '7Y+', '7Z+', '8X+', '8Y+', '8Z+', '9X+', '9Y+', '9Z+',
       '10X+', '10Y+', '10Z+'], dtype='<U4'))

The second approach to defining a CoordinateArray is hinted at in the representation we see when we type a CoordinateArray variable name into the terminal; we can pass a string array representing the CoordinateArray to the string_array argument. If proceeding with this approach, string_array must be explicitly specified as a keyword argument, otherwise the string array will be interpreted as the node argument and will try to be converted to an integer.

[12]:
# This will throw an error
coords = sdpy.coordinate_array(['1X+','2Z-'])
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[12], line 2
      1 # This will throw an error
----> 2 coords = sdpy.coordinate_array(['1X+','2Z-'])

File ~\Documents\Local_Repositories\sdynpy\src\sdynpy\core\sdynpy_coordinate.py:371, in coordinate_array(node, direction, structured_array, string_array, force_broadcast)
    369 # Create the coordinate array
    370 coord_array = CoordinateArray(bc_node.shape)
--> 371 coord_array.node = bc_node
    372 if not np.issubdtype(direction.dtype.type, np.integer):
    373     bc_direction = _map_direction_array(bc_direction)

File ~\Documents\Local_Repositories\sdynpy\src\sdynpy\core\sdynpy_array.py:146, in SdynpyArray.__setattr__(self, attr, value)
    141 except (ValueError, IndexError) as e:
    142     # # Check and make sure you don't have an attribute already with that
    143     # # name
    144     if attr in self.dtype.fields:
    145         # print('ERROR: Assignment to item failed, attempting to assign item to attribute!')
--> 146         raise e
    147     super().__setattr__(attr, value)

File ~\Documents\Local_Repositories\sdynpy\src\sdynpy\core\sdynpy_array.py:140, in SdynpyArray.__setattr__(self, attr, value)
    138 def __setattr__(self, attr, value):
    139     try:
--> 140         self[attr] = value
    141     except (ValueError, IndexError) as e:
    142         # # Check and make sure you don't have an attribute already with that
    143         # # name
    144         if attr in self.dtype.fields:
    145             # print('ERROR: Assignment to item failed, attempting to assign item to attribute!')

File ~\Documents\Local_Repositories\sdynpy\src\sdynpy\core\sdynpy_array.py:168, in SdynpyArray.__setitem__(self, key, value)
    166 try:
    167     if key in self.dtype.fields:
--> 168         self[key][...] = value
    169     else:
    170         super().__setitem__(key, value)

ValueError: invalid literal for int() with base 10: np.str_('1X+')

The when passing a string array, we must explicitly use the string_array keyword argument.

[13]:
coords = sdpy.coordinate_array(string_array = ['1X+','2Z-'])
coords
[13]:
coordinate_array(string_array=
array(['1X+', '2Z-'], dtype='<U3'))

Referencing Coordinates to Geometries

The entire point of the CoordinateArray is to store the location and the direction of the measurement or analysis data. Therefore, CoordinateArray objects are often used in conjuction with Geometry objects to specify the physical position and direction of the data in space. We will load in the Geometry object constructed in the previous document Geometry.

[14]:
geometry = sdpy.geometry.load('geometry.npz')

We may have hinted at the interaction of CoordinateArray objects with Geometry objects when we introduced the plot_coordinate method of the Geometry object. This was a method that plotted the geometry, but also drew arrows representing the local coordinate system directions. We will see that if we pass a CoordinateArray to this method, it will plot only those coordinates referenced. Various optional arguments define how the data is plotted.

[15]:
coords = sdpy.coordinate_array(string_array=['102Z+','204X+'])
geometry.plot_coordinate(coords, label_dofs=True, arrow_scale = 0.1)
[15]:
<sdynpy.core.sdynpy_geometry.GeometryPlotter at 0x28af1446180>

Two Coordinates on Geometry

This method allows us to very quickly see where a specific degree of freedom came from on the geometry.

Set Operations with CoordinateArrays

Because CoordinateArray objects are inherited from NumPy ndarray, many NumPy functions work well with CoordinateArray objects. NumPy has a class of operations called set operations which deal with sets of items. These are generally useful for CoordinateArray objects, because we often try to compare sets of degrees of freedom.

Unique Coordinates

One common operation is to get just the unique set of coordinates from a test or an analysis. One might have duplicated coordinates if combining several datasets together. NumPy’s unique function will give us just the unique degrees of freedom.

[16]:
coords = sdpy.coordinate_array([1,3,1,4],['X+','Y+','X+','Z+'])
coords
[16]:
coordinate_array(string_array=
array(['1X+', '3Y+', '1X+', '4Z+'], dtype='<U3'))
[17]:
unique_coords = np.unique(coords)
unique_coords
[17]:
coordinate_array(string_array=
array(['1X+', '3Y+', '4Z+'], dtype='<U3'))

Finding Coordinates in a Second Coordinate Array

A second common operation is to check which degrees of freedom from a test are in some master list of degrees of freedom. For this we can use NumPy’s isin function. This will give a logical array with True values wherever a degree of freedom was found in the master list.

[18]:
check_dofs = sdpy.coordinate_array([1,2,3,4],['X+','Y+','Z+'],force_broadcast=True)
check_dofs
[18]:
coordinate_array(string_array=
array(['1X+', '1Y+', '1Z+', '2X+', '2Y+', '2Z+', '3X+', '3Y+', '3Z+',
       '4X+', '4Y+', '4Z+'], dtype='<U3'))
[19]:
dofs_to_keep = sdpy.coordinate_array(string_array = ['1X+','2Z+','4Y+'])
dofs_to_keep
[19]:
coordinate_array(string_array=
array(['1X+', '2Z+', '4Y+'], dtype='<U3'))
[20]:
keep_indices = np.isin(check_dofs,dofs_to_keep)
keep_indices
[20]:
array([ True, False, False, False, False,  True, False, False, False,
       False,  True, False])

Finding Common Coordinates Between Two Datasets

One common operation is to get the common degrees of freedom between two datasets. Perhaps we are comparing test data to analysis, or two analyses with different modeling assumptions. We can only compare equivalent degrees of freedom, so we would like to know which degrees of freedom the two sets have in common.

[21]:
coords_1 = sdpy.coordinate_array([1,2,3,4],['X+','Y-','X+','Z+'])
coords_2 = sdpy.coordinate_array([1,2,4],['X+','Y+','Z+'])

We can use NumPy’s intersect1d function to get the intersection of the two sets of degrees of freedom.

[22]:
common_dofs = np.intersect1d(coords_1,coords_2)
common_dofs
[22]:
coordinate_array(string_array=
array(['1X+', '4Z+'], dtype='<U3'))

Sometimes when we would like to compare degrees of freedom, we don’t actually care about a difference in polarity, because we can simply flip the sign on the data when making the comparison. Taking the absolute value of a CoordinateArray turns any negative polarity (e.g. \(X-\)) to a positive polarity (e.g. \(X+\)). We can then make comparisons on the absolute values of the arrays.

[23]:
common_dofs_ignore_polarity = np.intersect1d(abs(coords_1),abs(coords_2))
common_dofs_ignore_polarity
[23]:
coordinate_array(string_array=
array(['1X+', '2Y+', '4Z+'], dtype='<U3'))

We see that if we ignore polarity, we find one more common coordinate between the two CoordinateArray objects.

Excluding Coordinates from a CoordinateArray

Sometimes we have a CoordinateArray object with many coordinates in it, and we want to exclude specific coordinates. NumPy’s setdiff1d will take the difference of two CoordinateArray objects, leaving only the coordinates which are not in the second array.

[24]:
coords_1
[24]:
coordinate_array(string_array=
array(['1X+', '2Y-', '3X+', '4Z+'], dtype='<U3'))
[25]:
coords_2
[25]:
coordinate_array(string_array=
array(['1X+', '2Y+', '4Z+'], dtype='<U3'))
[26]:
remaining_coords = np.setdiff1d(coords_1,coords_2)
remaining_coords
[26]:
coordinate_array(string_array=
array(['2Y-', '3X+'], dtype='<U3'))

Again, we could take the absolute value to ignore polarity.

[27]:
remaining_coords_ignore_polarity = np.setdiff1d(
    abs(coords_1),
    abs(coords_2))
remaining_coords_ignore_polarity
[27]:
coordinate_array(string_array=
array(['3X+'], dtype='<U3'))

Finding All Coordinates in Two CoordinateArrays

Finally, we may want to find all of the coordinates that exist between two CoordinateArray objects, though without duplicating items such as what might happen if we simply concatenated the arrays together. NumPy’s union function allows us to do just that.

[28]:
coords_1 = sdpy.coordinate_array(string_array = [
    '1X+','2Y+','3Z+','4RX+'])
coords_1
[28]:
coordinate_array(string_array=
array(['1X+', '2Y+', '3Z+', '4RX+'], dtype='<U4'))
[29]:
coords_2 = sdpy.coordinate_array(string_array = [
    '1RX+','2RZ-','3Z+','1X+'])
[30]:
all_coords = np.union1d(coords_1,coords_2)
all_coords
[30]:
coordinate_array(string_array=
array(['1X+', '1RX+', '2RZ-', '2Y+', '3Z+', '4RX+'], dtype='<U4'))

In the above, we see that the common coordinates \(1X+\) and \(3Z+\) do not appear more than once in the final array, which can come in handy.

Contrast this to concatination, where we simply append one CoordinateArray to the next CoordinateArray.

[31]:
all_coords_concat = np.concatenate((coords_1,coords_2))
all_coords_concat
[31]:
coordinate_array(string_array=
array(['1X+', '2Y+', '3Z+', '4RX+', '1RX+', '2RZ-', '3Z+', '1X+'],
      dtype='<U4'))

Here we see that the coordinates that appear in both CoordinateArray objects are now duplicated.

Sorting CoordinateArray Objects

Coordinate arrays in a random order can be difficult to parse, so it can be convenient to sort the CoordinateArray into a reasonable ordering. Because CoordinateArray objects are NumPy ndarray objects, we can simply use NumPy’s sort capability.

[32]:
coords = sdpy.coordinate_array(np.arange(5,1,-1)[:,np.newaxis], np.arange(-6,7)).flatten()
coords
[32]:
coordinate_array(string_array=
array(['5RZ-', '5RY-', '5RX-', '5Z-', '5Y-', '5X-', '5', '5X+', '5Y+',
       '5Z+', '5RX+', '5RY+', '5RZ+', '4RZ-', '4RY-', '4RX-', '4Z-',
       '4Y-', '4X-', '4', '4X+', '4Y+', '4Z+', '4RX+', '4RY+', '4RZ+',
       '3RZ-', '3RY-', '3RX-', '3Z-', '3Y-', '3X-', '3', '3X+', '3Y+',
       '3Z+', '3RX+', '3RY+', '3RZ+', '2RZ-', '2RY-', '2RX-', '2Z-',
       '2Y-', '2X-', '2', '2X+', '2Y+', '2Z+', '2RX+', '2RY+', '2RZ+'],
      dtype='<U4'))

We have two options for sorting. We can sort the array into a new array. This occurs when you use NumPy’s sort function.

[33]:
sorted_coords = np.sort(coords)
sorted_coords
[33]:
coordinate_array(string_array=
array(['2RZ-', '2RY-', '2RX-', '2Z-', '2Y-', '2X-', '2', '2X+', '2Y+',
       '2Z+', '2RX+', '2RY+', '2RZ+', '3RZ-', '3RY-', '3RX-', '3Z-',
       '3Y-', '3X-', '3', '3X+', '3Y+', '3Z+', '3RX+', '3RY+', '3RZ+',
       '4RZ-', '4RY-', '4RX-', '4Z-', '4Y-', '4X-', '4', '4X+', '4Y+',
       '4Z+', '4RX+', '4RY+', '4RZ+', '5RZ-', '5RY-', '5RX-', '5Z-',
       '5Y-', '5X-', '5', '5X+', '5Y+', '5Z+', '5RX+', '5RY+', '5RZ+'],
      dtype='<U4'))

We see that we have sorted the CoordinateArray by increasing node number then increasing direction (with the direction being its integer representation, so negative values (therefore polarities) come before positive ones.

We note that the original CoordinateArray has not changed due to the sorting operation.

[34]:
coords
[34]:
coordinate_array(string_array=
array(['5RZ-', '5RY-', '5RX-', '5Z-', '5Y-', '5X-', '5', '5X+', '5Y+',
       '5Z+', '5RX+', '5RY+', '5RZ+', '4RZ-', '4RY-', '4RX-', '4Z-',
       '4Y-', '4X-', '4', '4X+', '4Y+', '4Z+', '4RX+', '4RY+', '4RZ+',
       '3RZ-', '3RY-', '3RX-', '3Z-', '3Y-', '3X-', '3', '3X+', '3Y+',
       '3Z+', '3RX+', '3RY+', '3RZ+', '2RZ-', '2RY-', '2RX-', '2Z-',
       '2Y-', '2X-', '2', '2X+', '2Y+', '2Z+', '2RX+', '2RY+', '2RZ+'],
      dtype='<U4'))

If we wanted to sort the CoordinateArray in place, we can use its sort method.

[35]:
coords.sort()

The sort method of the CoordinateArray object does not return a value, instead it modifies the original array in-place. If we look at the CoordinateArray after the indexing operation, we see that it has been modified.

[36]:
coords
[36]:
coordinate_array(string_array=
array(['2RZ-', '2RY-', '2RX-', '2Z-', '2Y-', '2X-', '2', '2X+', '2Y+',
       '2Z+', '2RX+', '2RY+', '2RZ+', '3RZ-', '3RY-', '3RX-', '3Z-',
       '3Y-', '3X-', '3', '3X+', '3Y+', '3Z+', '3RX+', '3RY+', '3RZ+',
       '4RZ-', '4RY-', '4RX-', '4Z-', '4Y-', '4X-', '4', '4X+', '4Y+',
       '4Z+', '4RX+', '4RY+', '4RZ+', '5RZ-', '5RY-', '5RX-', '5Z-',
       '5Y-', '5X-', '5', '5X+', '5Y+', '5Z+', '5RX+', '5RY+', '5RZ+'],
      dtype='<U4'))

If the fact that the degree of freedom \(RZ-\) (-6) is sorted so far from \(RZ+\) (+6), then once again we can take the absolute value prior to sorting. Note that if we take the absolute value and then call the sort method , which modifies the array in place, without first storing the absolute value to a variable, apparently nothing will happen. This is because the output of the absolute value of the CoordinateArray is a new array, and if we modify the new array in place without it being stored, the changes are immediately garbage-collected by Python.

[37]:
abs(coords).sort()
coords
[37]:
coordinate_array(string_array=
array(['2RZ-', '2RY-', '2RX-', '2Z-', '2Y-', '2X-', '2', '2X+', '2Y+',
       '2Z+', '2RX+', '2RY+', '2RZ+', '3RZ-', '3RY-', '3RX-', '3Z-',
       '3Y-', '3X-', '3', '3X+', '3Y+', '3Z+', '3RX+', '3RY+', '3RZ+',
       '4RZ-', '4RY-', '4RX-', '4Z-', '4Y-', '4X-', '4', '4X+', '4Y+',
       '4Z+', '4RX+', '4RY+', '4RZ+', '5RZ-', '5RY-', '5RX-', '5Z-',
       '5Y-', '5X-', '5', '5X+', '5Y+', '5Z+', '5RX+', '5RY+', '5RZ+'],
      dtype='<U4'))

We can see that coords is unchanged. To get the sorted array, we can either store the absolute value as an intermediate step, or use the sort function to create a new sorted list without modifying the original in place.

[38]:
# Modifying in place
abs_coords = abs(coords)
abs_coords.sort()
abs_coords
[38]:
coordinate_array(string_array=
array(['2', '2X+', '2X+', '2Y+', '2Y+', '2Z+', '2Z+', '2RX+', '2RX+',
       '2RY+', '2RY+', '2RZ+', '2RZ+', '3', '3X+', '3X+', '3Y+', '3Y+',
       '3Z+', '3Z+', '3RX+', '3RX+', '3RY+', '3RY+', '3RZ+', '3RZ+', '4',
       '4X+', '4X+', '4Y+', '4Y+', '4Z+', '4Z+', '4RX+', '4RX+', '4RY+',
       '4RY+', '4RZ+', '4RZ+', '5', '5X+', '5X+', '5Y+', '5Y+', '5Z+',
       '5Z+', '5RX+', '5RX+', '5RY+', '5RY+', '5RZ+', '5RZ+'], dtype='<U4'))
[39]:
abs_coords_sorted = np.sort(abs(coords))
abs_coords_sorted
[39]:
coordinate_array(string_array=
array(['2', '2X+', '2X+', '2Y+', '2Y+', '2Z+', '2Z+', '2RX+', '2RX+',
       '2RY+', '2RY+', '2RZ+', '2RZ+', '3', '3X+', '3X+', '3Y+', '3Y+',
       '3Z+', '3Z+', '3RX+', '3RX+', '3RY+', '3RY+', '3RZ+', '3RZ+', '4',
       '4X+', '4X+', '4Y+', '4Y+', '4Z+', '4Z+', '4RX+', '4RX+', '4RY+',
       '4RY+', '4RZ+', '4RZ+', '5', '5X+', '5X+', '5Y+', '5Y+', '5Z+',
       '5Z+', '5RX+', '5RX+', '5RY+', '5RY+', '5RZ+', '5RZ+'], dtype='<U4'))

One issue with the above is that we have lost the polarity references of the original list. For example, we have two \(2X+\) coordinates in the final CoordinateArray, one from the \(2X+\) coordinate and one from the \(2X-\) coordinate. If we wish to maintain the polarities in the final CoordinateArray, we can use NumPy’s argsort function, which returns the indices of the array in the order that would sort the array. We can then use these indices to index the original array.

[40]:
sorted_indices = np.argsort(abs(coords))
sorted_indices
[40]:
array([ 6,  5,  7,  4,  8,  3,  9,  2, 10, 11,  1, 12,  0, 19, 18, 20, 17,
       21, 16, 22, 15, 23, 24, 14, 25, 13, 32, 31, 33, 30, 34, 29, 35, 36,
       28, 27, 37, 38, 26, 45, 44, 46, 43, 47, 48, 42, 49, 41, 50, 40, 39,
       51])
[41]:
coords_sorted = coords[sorted_indices]
coords_sorted
[41]:
coordinate_array(string_array=
array(['2', '2X-', '2X+', '2Y-', '2Y+', '2Z-', '2Z+', '2RX-', '2RX+',
       '2RY+', '2RY-', '2RZ+', '2RZ-', '3', '3X-', '3X+', '3Y-', '3Y+',
       '3Z-', '3Z+', '3RX-', '3RX+', '3RY+', '3RY-', '3RZ+', '3RZ-', '4',
       '4X-', '4X+', '4Y-', '4Y+', '4Z-', '4Z+', '4RX+', '4RX-', '4RY-',
       '4RY+', '4RZ+', '4RZ-', '5', '5X-', '5X+', '5Y-', '5Y+', '5Z+',
       '5Z-', '5RX+', '5RX-', '5RY+', '5RY-', '5RZ-', '5RZ+'], dtype='<U4'))

In this latter case, because we are comparing, for example \(2X+\) to \(2X+\) when we sorted the absolute value of the array, in the final array, the first item of these equivalent arrays will be the one that came first in the original array. Note that because \(2X-\) came before \(2X+\) in coords, it will also come before it in the sorted version of coords when the sorting is done based on the absolute value.

Converting CoordinateArrays to Strings

While we can get the representation of a CoordinateArray by typing it into the console, this is not very useful for putting that CoordinateArray into a document or something like that. Therefore, SDynPy gives a way to convert the CoordinateArray object into a string array, which can then be exported or formatted however the user wishes.

[42]:
coords
[42]:
coordinate_array(string_array=
array(['2RZ-', '2RY-', '2RX-', '2Z-', '2Y-', '2X-', '2', '2X+', '2Y+',
       '2Z+', '2RX+', '2RY+', '2RZ+', '3RZ-', '3RY-', '3RX-', '3Z-',
       '3Y-', '3X-', '3', '3X+', '3Y+', '3Z+', '3RX+', '3RY+', '3RZ+',
       '4RZ-', '4RY-', '4RX-', '4Z-', '4Y-', '4X-', '4', '4X+', '4Y+',
       '4Z+', '4RX+', '4RY+', '4RZ+', '5RZ-', '5RY-', '5RX-', '5Z-',
       '5Y-', '5X-', '5', '5X+', '5Y+', '5Z+', '5RX+', '5RY+', '5RZ+'],
      dtype='<U4'))

By calling the string_array method of the CoordinateArray object, SDynPy will export the CoordinateArray as an array of strings.

[43]:
string_array = coords.string_array()
string_array
[43]:
array(['2RZ-', '2RY-', '2RX-', '2Z-', '2Y-', '2X-', '2', '2X+', '2Y+',
       '2Z+', '2RX+', '2RY+', '2RZ+', '3RZ-', '3RY-', '3RX-', '3Z-',
       '3Y-', '3X-', '3', '3X+', '3Y+', '3Z+', '3RX+', '3RY+', '3RZ+',
       '4RZ-', '4RY-', '4RX-', '4Z-', '4Y-', '4X-', '4', '4X+', '4Y+',
       '4Z+', '4RX+', '4RY+', '4RZ+', '5RZ-', '5RY-', '5RX-', '5Z-',
       '5Y-', '5X-', '5', '5X+', '5Y+', '5Z+', '5RX+', '5RY+', '5RZ+'],
      dtype='<U4')

We can then use Python’s significant capabilities for string operations to format the string how we would like.

[44]:
', '.join([string.lower() for string in string_array])
[44]:
'2rz-, 2ry-, 2rx-, 2z-, 2y-, 2x-, 2, 2x+, 2y+, 2z+, 2rx+, 2ry+, 2rz+, 3rz-, 3ry-, 3rx-, 3z-, 3y-, 3x-, 3, 3x+, 3y+, 3z+, 3rx+, 3ry+, 3rz+, 4rz-, 4ry-, 4rx-, 4z-, 4y-, 4x-, 4, 4x+, 4y+, 4z+, 4rx+, 4ry+, 4rz+, 5rz-, 5ry-, 5rx-, 5z-, 5y-, 5x-, 5, 5x+, 5y+, 5z+, 5rx+, 5ry+, 5rz+'

Saving and Loading CoordinateArrays

Like all SdynpyArray subclasses, CoordinateArray is actually a NumPy ndarray, meaning it can be trivially stored to a NumPy .npy file. SDynPy therefore implements save and load methods for the CoordinateArray objects to save to and read from the disk.

[45]:
coords
[45]:
coordinate_array(string_array=
array(['2RZ-', '2RY-', '2RX-', '2Z-', '2Y-', '2X-', '2', '2X+', '2Y+',
       '2Z+', '2RX+', '2RY+', '2RZ+', '3RZ-', '3RY-', '3RX-', '3Z-',
       '3Y-', '3X-', '3', '3X+', '3Y+', '3Z+', '3RX+', '3RY+', '3RZ+',
       '4RZ-', '4RY-', '4RX-', '4Z-', '4Y-', '4X-', '4', '4X+', '4Y+',
       '4Z+', '4RX+', '4RY+', '4RZ+', '5RZ-', '5RY-', '5RX-', '5Z-',
       '5Y-', '5X-', '5', '5X+', '5Y+', '5Z+', '5RX+', '5RY+', '5RZ+'],
      dtype='<U4'))
[46]:
coords.save('coords.npy')
[47]:
loaded_coords = coords.load('coords.npy')
loaded_coords
[47]:
coordinate_array(string_array=
array(['2RZ-', '2RY-', '2RX-', '2Z-', '2Y-', '2X-', '2', '2X+', '2Y+',
       '2Z+', '2RX+', '2RY+', '2RZ+', '3RZ-', '3RY-', '3RX-', '3Z-',
       '3Y-', '3X-', '3', '3X+', '3Y+', '3Z+', '3RX+', '3RY+', '3RZ+',
       '4RZ-', '4RY-', '4RX-', '4Z-', '4Y-', '4X-', '4', '4X+', '4Y+',
       '4Z+', '4RX+', '4RY+', '4RZ+', '5RZ-', '5RY-', '5RX-', '5Z-',
       '5Y-', '5X-', '5', '5X+', '5Y+', '5Z+', '5RX+', '5RY+', '5RZ+'],
      dtype='<U4'))

Using CoordinateArrays with Other SDynPy Objects

Every SDynPy object uses Coordinate to do its bookkeeping. NDDataArray objects keep track of which data is associated with each coordinate. ShapeArray objects keep track of the coordinates associated with each of their degrees of freedom. System objects keep track of the coordinates associated with the rows and columns of the system matrices.

These concepts and more will be described more fully in subsequent documentation pages.

Summary

This document has described the CoordinateArray object in SDynPy, which is used to track degree of freedom information in many of the other SDynPy objects. We showed how to create CoordinateArray objects from scratch, using both node/direction definitions, as well as string_array definitions. We showed how we could use broadcasting with the node/direction definition, otherwise we could also provide the force_broadcast=True keyword argument.

We showed how CoordinateArray objects refer to the displacement coordinate systems of the nodes in a Geometry object. We showed how we could easily visualized CoordinateArray objects on the Geometry by using its plot_coordinate method and passing a CoordinateArray object.

We discussed numerous set operations which could find unique coordinates and perform intersections, differences, and unions of coordinates in different CoordinateArray objects.

We showed how we could convert a CoordinateArray object to a simple string array. We also showed how we can save or load CoordinateArray objects.

Finally, we mentioned that CoordinateArray objects are used all across SDynPy as a way to track the coordinates associated with specific pieces of data. We will explore one of these types of objects in the next section.