Numeric types¶
InertialSim makes extensive use of NumPy for numeric data types and operations. Unfamiliar users should consult the NumPy guide: https://numpy.org/doc/stable/user/index.html for a quick start and comparison with other tools.
Basic InertialSim types are the Python standard library types (int,float,tuple, etc.) and NumPy arrays (numpy.ndarray). All floating point types use 64-bit precision (numpy.float64).
Array modification, views, and copies
Arrays are mutable types. Many InertialSim functions modify (or mutate) their inputs or return views to subsets of the inputs. This is similar to passing by reference in other languages. InertialSim methods that take numpy.typing.ArrayLike inputs make a copy since they are typically modified to meet InertialSim conventions (shape, data types, range, etc.). Methods that take InertialSim custom types (e.g. inertialsim.geometry classes) do not copy their inputs and do not modify their inputs. Users should be cautious of unexpected side-effects when modifying data that has already been passed to InertialSim.
Unit conventions¶
With the exception of specifications, all numeric values
input and output from InertialSim use SI units (meter
, kilogram
, second
,
etc.). All angles use units of radians
.
Array conventions¶
NumPy arrays support linear algebra but are not primarily designed for linear algebra. As a result, the following conventions are applied.
Input and output types¶
User inputs are typically of type numpy.typing.ArrayLike. These are types that are already a NumPy array or can be easily converted into an array. Examples include:
import numpy as np
array = [1, 2] # Python list
array = (1, 2, 3, 4) # Python tuple
array = np.array([1, 2, 3]) # 1D numpy array
array = np.array([[1], [2], [3]]) # 2D numpy array
array = [[1, 2, 3], [4, 5, 6]] # List of lists, or row-major matrix
InertialSim outputs are typically of type numpy.typing.NDArray. These are simply NumPy arrays. Lists, tuples, and other array-like inputs are converted explicitly such that all outputs are arrays.
Multiplication operators¶
Scalar (element-wise) multiplication is performed with the *
operator. Linear
algebra multiplication (matrix-vector, matrix-matrix) is performed with the @
operator.
Array shapes¶
Regardless of input shape (list, 1-dimensional array, 2-dimensional array, etc.) all inputs are converted to 3-dimensional arrays internally. All array type outputs are 3-dimensional also. This is to support stacking of inputs and outputs (typically over a time axis) and vectorization (operations without for-loops).
Vector terminology
The term "vector" has many meanings. The most fundamental is a physical
vector, for example, Earth's gravitational force at a given point in space.
The second is the numerical representation of a physical vector as a 3-tuple
of values for
loops are replaced in software by array operations.
This last meaning can take advantage of vector processors (SIMD, GPU, etc.)
but is not required to. See
https://en.wikipedia.org/wiki/Array_programming.
Stacking multiple scalars, vectors, or matrices¶
The NumPy (and Python) convention for stacking multiple scalars, vectors or
matrices is to increment the first index (see numpy.matmul for example).
Therefore stacking five matrices with 3 rows and 4 columns each results in an
array of shape = (5,3,4)
. Stacking ten scalars results in an array of shape
= (10,1,1)
. Stacking two quaternions results in an array of shape = (2,4,1)
.
Example: multiplying two vectors by the same matrix¶
Firstly, we illustrate the need for the 3-dimensional convention. We wish to
vectorize the multiplication of one 3x3 matrix with two 3x1 vectors. Using
minimum dimension arrays and the @
operator does not work in this instance.
NumPy's broadcast
behavior complains (reasonably) that the two arrays have mismatched inner
dimensions (shape = (3,3) @ shape = (2,3))
.
# Two vectors (shape = (2,3))
vectors = np.array([[1, 2, 3], [4, 5, 6]])
# Single matrix (shape = (3,3))
matrix = np.array([[[0, 1, 0], [1, 0, 0], [0, 0, 1]]])
# Matrix-vector multiplication results in an error
matrix @ vectors
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
Cell In[2], line 8
5 matrix = np.array([[[0, 1, 0], [1, 0, 0], [0, 0, 1]]])
7 # Matrix-vector multiplication results in an error
----> 8 matrix @ vectors
ValueError: matmul: Input operand 1 has a mismatch in its core dimension 0, with gufunc signature (n?,k),(k,m?)->(n?,m?) (size 2 is different from 3)
Making both inputs 3-dimensional fixes this issue. Now the multiplication is
shape = (1,3,3) @ shape = (2,3,1)
and the broadcast behavior works as
expected. This is true in general for vectorizing basic linear algebra
operations.
# Two vectors (shape = (2,3,1))
vectors = np.array([[[1], [2], [3]], [[4], [5], [6]]])
# One matrix (shape = (1,3,3))
matrix = np.array([[[0, 1, 0], [1, 0, 0], [0, 0, 1]]])
# Matrix-vector multiplication results in two new vectors (shape = (2,3,1)) that
# are permutations of the input as expected
result = matrix @ vectors
result
array([[[2],
[1],
[3]],
[[5],
[4],
[6]]])
For a single vector or matrix output, it may be undesirable or incompatible with other libraries to maintain extra dimensions. Users can recover a minimum dimensional array with numpy.squeeze.
Convenience functions¶
The inertialsim.geometry module offers three convenience functions if users wish to pre-convert their data to InertialSim array conventions: scalar_array, vector_array, and matrix_array.
from inertialsim import geometry
# Input a list of integers
scalars = [1, 2, 3, 4]
# Outputs an array with shape = (4,1,1) and dtype = np.float64
scalars = geometry.scalar_array(scalars)
# Input a list of 2 3-dimensional vectors
vectors = [[1, 2, 3], [4, 5, 6]]
# Outputs an array with shape = (2,3,1) and dtype = np.float64
vectors = geometry.vector_array(vectors, dimension=3)
# Input one 4x3 matrix
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12]]
# Outputs an array with shape = (1,4,3) and dtype = np.float64
matrix = geometry.matrix_array(matrix, rows=4, columns=3)