Skip to content

SETI/rms-polymath

GitHub release; latest by date GitHub Release Date Test Status Documentation Status Code coverage
PyPI - Version PyPI - Format PyPI - Downloads PyPI - Python Version
GitHub commits since latest release GitHub commit activity GitHub last commit
Number of GitHub open issues Number of GitHub closed issues Number of GitHub open pull requests Number of GitHub closed pull requests
GitHub License Number of GitHub stars GitHub forks

Introduction

PolyMath expands on the NumPy module and introduces a variety of additional data types and features to simplify 3-D geometry calculations. It is a product of the the PDS Ring-Moon Systems Node.

Installation

The PolyMath module is available via the rms-polymath package on PyPI and can be installed with:

pip install rms-polymath

Getting Started

The typical way to use this is just to include this line in your programs:

import polymath

or

from polymath import (Boolean, Matrix, Matrix3, Pair, Quaternion, Qube, Scalar, Unit,
                      Vector, Vector3)

Features

The PolyMath classes are:

  • Scalarimage: A single zero-dimensional number.
  • Vectorimage: An arbitrary 1-D object.
  • Pairimage: A subclass of Vector representing a vector with two coordinates.
  • Vector3image: A subclass of Vector representing a vector with three coordinates.
  • Matriximage: An arbitrary 2-D matrix.
  • Matrix3image: A subclass of Matrix representing a unitary 3x3 rotation matrix.
  • Quaternionimage: A subclass of Vector representing a 4-component quaternion.
  • Booleanimage: A True or False value.
  • Qubeimage: The superclass of all of the above, supporting objects of arbitrary dimension.

Importantly, each of these classes can represent not just a single object, but also an arbitrary array of these objects. Mathematical operations on arrays follow NumPy's rules of "broadcasting"; see https://numpy.org/doc/stable/user/basics.broadcasting.html for these details. The key advantage of PolyMath's approach is that it separates each object's shape from any internal dimensions (e.g., (3,3) for a Vectorimage object), largely eliminating any need to ever do "index bookkeeping". For example, suppose S is a Scalar and V is a Vectorimage. Then, in PolyMath, you can write:

S * V

whereas, in NumPy, you would have to write:

S[..., np.newaxis] * V

to get the same result. This capability makes it possible to write out algorithms as if each operation is on a single vector, scalar, or matrix, ignoring the internal dimensionality of each PolyMath object.

PolyMath has the following additional features:

  • Derivatives: An object can have arbitrary derivatives, or an array thereof. These get carried through all math operations so, for example, if X.d_dt is the derivative of X with respect to t, then X.sin().d_dt is the derivative of sin(X) with respect to t. This means that you can write out math operations in the most straightforward manner, without worrying about how any derivatives get calculated.
  • Masks: Objects can have arbitrary boolean masks, which are equal to True where the object's value is undefined. This is similar to NumPy's MaskedArray class, but with added capabilities. For example, if an object is mostly masked, you can use the shrinkimage method to speed up math operations by excluding all the masked elements.
  • Units: Objects can have arbitrary units, as defined by PolyMath's Unit class.
  • Read-only status: It is easy to define an object to be read-only, which will then prevent it from being modified further. This can be useful for preventing NumPy errors that can arise when multiple objects share memory (a common situation) and one of them gets modified by accident.
  • Indexing: An object can be indexed in a variety of ways that expand upon NumPy's indexing rules.
  • Pickling: Python's pickle module can be used to save and re-load objects in a way that makes for extremely efficient storage.

PolyMath provides the mathematical underpinnings of the OOPS Library. As an illustration of its power, here are some examples of how OOPS uses PolyMath objects to describe a data product:

  • los is a single Vectorimage that represents the lines of sight represented by each sample in the data product. If the product is a 1000x1000 image, then los will have an internal shape of (1000,1000). If the product is a single detector, then los will have no internal shape.

  • time is a Scalarimage that represents the time that the photons arrived at the detector. If the instrument is a simple, shuttered camera, then all photons arrive at the same time and time can have a single value. However, if a raster-scanning device obtains a 1000x1000 image, then time will have an internal shape of (1000,1000), with each element representing the unique arrival time at that pixel. For a "pushbroom" camera, the detector receives the image line by line, so time will have an internal shape of (1000,1) or (1,1000), depending on the orientation of the detector.

  • cmatrix is a Matrix3image that represents the rotation matrix from the instrument's internal coordinates to the J2000 frame, which is fixed relative to the sky. If the instrument is not rotating during an observation, then a single matrix is required. However, if the camera is on a rotating platform and it samples photons at different times, then cmatrix may need to have an internal shape of (1000,1000) to describe a 1000x1000 image. Furthermore, the rate of change of cmatrix can be described by a derivative with respect to time. When one calculates where the lines of sight intercepted a target body, the time derivative of cmatrix can then be used to determine the amount of smear in the image.

Although different types of data products might have very different internal representations, PolyMath makes it possible to write a geometry calculation just once and then re-use it for all of these situations.

As a specific example, OOPS can determine where each line of sight sampled by a data product intercepted the surface of a particular planetary body. The intercept points are represented by a single Vectorimage given in the body's coordinate frame. From this object, it is straightforward to derive latitude, longitude, emission angle, etc. If the product is a 1000x1000 image, then the Vectorimage will have an internal shape of (1000,1000). Furthermore, if the body does not entirely fill the field of view, the lines of sight that did not intercept the body are masked. It is not uncommon for a body to only partially fill a field of view. In this case, OOPS can speed up calculations, sometimes substantially, by omitting the masked elements of the Vectorimage object.

Math Operations

All standard mathematical operators and indexing/slicing options are defined for PolyMath objects, where appropriate: +, -, *, /, %, //,**, along with their in-place variants. Equality tests ==, != are available for all objects; comparison operators <, <=, >, >= are supported for Scalars and Booleans. Where appropriate, methods such as abs()image, len()image, mean()image, sum()image, identity()image, reciprocal()image, int()image, Matrix.inverse()image, and Matrix.transpose()image (or property Matrix.T).

Scalarimage: support most common math functions such as sin()image, cos()image, tan()image, arcsin()image, arccos()image, arctan()image, arctan2()image, sqrt()image, log()image, exp()image, sign()image, int()image, frac()image, min()image, max()image, argmin()image, argmax()image, minimum()image, maximum()image, median()image, and sort()image. It also supports quadratic equations via solve_quadratic()image and eval_quadratic()image.

Vectorimage: support functions such as norm()image, norm_sq()image, unit()image, dot()image, cross()image, ucross()image, outer()image (outer product), perp()image (perpendicular vector), proj()image (projection), and sep()image (separation angle). Element-by-element operations are also supported using element_mul()image and element_div()image.

Vector3image: supports additional functions such as from_ra_dec_length()image, to_ra_dec_length()image, from_cylindrical()image, to_cylindrical()image, longitude()image, latitude()image, spin()image (rotate one vector about another), and offset_angles()image (the angles from the three primary axes).

Pairimage: supports additional functions such as swapxy()image, rot90()image (for rotation by a multiple of 90 degrees), and angle()image (for the vector's angular direction).

Matrix3image: functions rotate()image and unrotate()image apply a rotation to another object. Methods x_rotation()image, y_rotation()image, z_rotation()image, axis_rotation()image, pole_rotation()image, from_euler()image, and unitary()image are convenient, alternative ways to define a rotation matrix. Use to_euler()image and to_quaternion()image to reverse these definitions.

Quaternionimage: supports functions such as conj()image (conjugate), to_parts()image (for the Scalar and Vector3 components), from_parts()image (to construct from the Scalar and Vector3 components), to_rotation()image (for the transform as a direction Vector3 and rotation angle), from_matrix3()image, to_matrix3()image, from_euler()image, and to_euler()image.

The values (or vals) property of each object returns its value as a NumPy array. For Scalar objects with no shape, values is a Python-native value of float or int; for Boolean objects with no shape, values is a Python-native bool.

One can generally mix PolyMath arithmetic with scalars, NumPy ndarrays, NumPy MaskedArrays, or anything array-like.

Shapes and Broadcasting

The PolyMath subclasses, e.g., Scalarimage, Vector3image, and Matrix3image, define one or more possibly multidimensional items. Unlike NumPy ndarrays, this class makes a clear distinction between the dimensions associated with the items and any additional, leading dimensions that define an array of such items.

Each object's shape property contains the shape of its leading axes, whereas its item property defines the shape of individual elements. For example, a 2x2 array of Vectorimage objects would have shape=(2,2) and items=(3,3). Its values property would be a NumPy array with shape (2,2,3,3). In addition, ndims (or ndim) contains the number of dimensions in the shape; size is the number of items; rank is the number of dimensions in the items; isize is the number of item elements.

To change the shape of an object, use methods reshape()image, flatten()image, move_axis()image, roll_axis()image, and swap_axes()image. These all return a shallow copy of an object, which shares memory with the original object.

Standard NumPy rules of broadcasting apply, but only on the shape dimensions, not on the item dimensions. For example, if you multiply a Matrix3image with shape (2,2) by a Vectorimage object with shape (5,1,2), you would get a new (rotated) Vector object with shape (5,2,2). See the complete rules of broadcasting here: https://numpy.org/doc/stable/user/basics.broadcasting.html

If necessary, you can explicitly broadcast objects to a new shape using methods broadcast()image and broadcast_to()image; these return shallow, read-only copies. Use broadcasted_shape()image to determine the shape that will result from an operation involving multiple PolyMath objects and NumPy arrays.

Derivatives

PolyMath objects can track associated derivatives and partial derivatives, which are represented by a dictionary of other PolyMath objects. A common use case is to let X be a Vectorimage: object describing the position of one or more bodies or points on a surface and to use the time-derivative of X to describe the velocity. The derivs property of an object is a dictionary of each derivative, keyed by its name. For example, a time-derivative is often keyed by t. For convenience, you can also reference each derivative by the attribute name "d_d" plus the key, so the time-derivative of X can be accessed via X.d_dt. This feature makes it possible to write an algorithm describing the positions of bodies or features, and to have any time-derivatives carried along in the calculation with no additional programming effort.

The denominators of partial derivatives are represented by splitting the item shape into a numerator shape plus a denominator shape. As a result, for example, the partial derivatives of a Vectorimage object (item shape (3,)) with respect to a :class:Pair (item shape (2,)) will have overall item shape (3,2). Properties numer gives the numerator shape, nrank is the number of dimensions, and nsize gives the number of elements. Similarly, denom is the denominator shape, drank is the number of dimensions, and dsize is the number of elements.

The PolyMath subclasses generally do not constrain the shape of the denominator, just the numerator. As a result, the aforementioned partial derivatives can still be represented by a Vectorimage object.

Methods insert_deriv()image, insert_derivs()image, delete_deriv()image, delete_derivs()image, and rename_deriv()image can be used to add, remove, or modify derivatives after it has been constructed. You can also obtain a shallow copy of an object with one or more derivatives removed using without_deriv()image and without_derivs()image. For convenience, the wod property is equivalent to without_derivs()image. Note that the presence of derivatives inside an object can slow computational performance significantly, so it can useful to suppress derivatives from a calculation if they are not needed. Note that many math functions have a recursive option that defaults to True; set it to False to ignore derivatives within the given calculation.

A number of methods are focused on modifying the numerator and denominator components of objects: extract_numer()image, extract_denom()image, extract_denoms()image, slice_numer()image, transpose_numer()image, reshape_numer()image, flatten_numer()image, transpose_denom()image, reshape_denom()image, flatten_denom()image, join_items()image, split_items()image, and swap_items()image.

The function chain()image can be used for chain-multiplication of derivatives; this is also implemented via the @ operator.

Masks

Every object has a boolean mask, which identifies undefined values or array elements. Operations that would otherwise raise errors such as 1/0 and sqrt(-1) are masked, so that run-time warnings and exceptions can be avoided. A common use case is to have a "backplane" array describing the geometric content of a planetary image, where the mask identifies the pixels that do not intersect the surface.

Each object has a property mask, which contains the mask. A single value of False means that the object is entirely unmasked; a single value of True means it is entirely masked. Otherwise, mask is a NumPy array with boolean values, with a shape that matches that of the object itself (excluding its items). For example, a Vectorimage object with shape (5,2) could have a mask value represented by a NumPy array of shape (5,2), even though its values property has shape (5,2,3).

The mvals property of an object returns its values property as a NumPy MaskedArray.

Each object also has a property antimask, which is the "logical not" of the mask. Use the antimask as an index to select only the unmasked elements of an object. You can also use the shrink()image method to temporarily eliminate masked elements from an object, potentially speeding up calculations; use unshrink()image when you are done to restore the original shape.

Under normal circumstances, a masked value should be understood to mean, "this value does not exist." For example, a calculation of observed intercept points on a moon is masked if a line of sight missed the moon, because that line of sight does not exist. This is similar to NumPy's not-a-number ("NaN") concept, but there are important differences. For example,

  • Two masked values of the same class are considered equal. This is different from the behavior of NaN.
  • Unary and binary operations involving masked values always return masked values.
  • Because masked values are treated as if they do not exist, max()image returns the maximum among the unmasked values; all()image returns True if all the unmasked values are True (or nonzero).

However, PolyMath also provides boolean methods to support an alternative interpretation of masked values as indeterminate rather than nonexistent. These follow the rules of "three-valued logic:

  • tvl_and()image returns False if one value is False but the other is masked, because the result would be False regardless of the second value.
  • tvl_or()image returns True if one value is True but the other is masked, because the result would be True regardless of the second value.
  • tvl_all()image returns True only if and only all values are True; if any value is False, it returns False; if the only values are True or indeterminate, its value is indeterminate (meaning masked).
  • tvl_any()image returns True if any value is True; it returns False if every value is False; if the only values are False or indeterminate, its value is indeterminate.
  • tvl_eq()image and tvl_ne()image are indeterminate if either value is indeterminate.

You can only define an object's mask at the time it is constructed. To change a mask, use remask()image or remask_or()image, which return a shallow copy of the object (sharing memory with the original) but with a new mask. You can also use a variety of methods to construct an object with a new mask: mask_where()image, mask_where_eq()image, mask_where_ge()image, mask_where_gt()image, mask_where_le()image, mask_where_lt()image, mask_where_ne()image, mask_where_between()image, mask_where_outside()image, and clip()image.

Units

PolyMath objects also support embedded unit using the Unitimage class. However, the internal values in a PolyMath object are always held in standard units of kilometers, seconds and radians, or arbitrary combinations thereof. The unit is primarily used to affect the appearance of numbers during input and output. The unit_ or units property of any object will reveal the Unitimage object, or possibly None if the object is unitless.

A Unitimage allows for exact conversions between units. It is described by three integer exponents applying to dimensions of length, time, and angle. Conversion factors are describe by three (usually) integer values representing a numerator, denominator, and an exponent on pi. For example, Unit.DEGREE is represented by exponents (0,0,1) and factors (1,180,1), indicating that the conversion factor is pi/180. Most other common units are described by class constants; see the Unit class for details.

Normally, you specify the unit of an object at the time it is constructed. However, the method set_unit()image is available to set the unit property afterward.

Read-only Objects

PolyMath objects can be either read-only or read-write. Read-only objects are prevented from modification to the extent that Python makes this possible. Operations on read-only objects should always return read-only objects.

The as_readonly()image method can be used to set an object (and its derivatives) to read-only status. It is not possible to convert an object from read-only back to read-write; use copy()image instead. The readonly property is True if the object is read-only; False if it is read-write.

Alternative Constructors

Aside from the explicit constructor methods, numerous methods are available to construct objects from other objects. Methods include copy()image, clone()image, cast()image, zeros()image, ones()image, filled()image, as_this_type()image, as_all_constant()image, as_size_zero()image, masked_single()image, as_numeric()image, as_float()image, as_int()image, and as_bool()image. There are also methods to convert between classes, such as Vector.from_scalars()image, Vector.to_scala()rimage, Vector.to_scalars()image, Matrix3.twovec()image, (a rotation matrix defined by two vectors), Matrix.row_vector()image, Matrix.row_vectors()image, Matrix.column_vector()image, Matrix.column_vectors()image, and Matrix.to_vector()image,

Indexing

Using an index on a Qube object is very similar to using one on a NumPy array, but there are a few important differences. For purposes of retrieving selected values from an object:

  • True and False can be applied to objects with shape (). True leaves the object unchanged; False masks the object.

  • An index of True selects the entire associated axis in the object, equivalent to a colon or slice(None). An index of False reduces the associated axis to length one and masks the object entirely.

  • A Booleanimage: object can be used as an index. If this index is unmasked, it is the same as indexing with a boolean NumPy ndarray. If it is masked, the values of the returned object are masked wherever the Boolean's value is masked. When using a Boolean index to set items inside an object, locations where the Boolean index are masked are not changed.

  • A Scalarimage: object composed of integers can be used as an index. If this index is unmasked, it is equivalent to indexing with an integer or integer NumPy ndarray. If it is masked, the values of the returned object are masked wherever the Scalar masked. When using a Scalar index to set items inside an object, locations where the Scalar index are masked are not changed.

  • A Pairimage: object composed of integers can be used as an index. Each (i,j) value is treated is the index of two consecutive axes, and the associated value is returned. Where the Pair is masked, a masked value is returned. Similarly, a Vectorimage: with three or more integer elements is treated as the index of three or more consecutive axes.

  • As in NumPy, an integer valued array can be used to index a PolyMath object. In this case, the shape of the index array appears within the shape of the returned object. For example, if A has shape (6,7,8,9) and B has shape (3,1), then A[B] has shape (3,1,7,8,9); A[:,B] has shape (6,3,1,8,9), and A[...,B] has shape (6,7,8,3,1).

  • When multiple arrays are used for indexing at the same time, the broadcasted shape of these array appears at the location of the first array-valued index. In the same example as above, suppose C has shape (4,). Then A[B,C] has shape (3,4,8,9), A[:,B,C] has shape (6,3,4,9), and A[:,B,:,C] has shape (6,3,4,8). Note that this behavior is slightly different from how NumPy handles indexing with multiple arrays.

Several methods can be used to convert PolyMath objects to objects than be used for indexing NumPy arrays. You can obtain integer indices from Scalar.as_index()image, Vector.as_index()image, Scalar.as_index_and_mask()image, and Vector.as_index_and_mask()image. You can obtain boolean masks from Boolean.as_index()image, as_mask_where_nonzero()image, as_mask_where_zero()image, as_mask_where_nonzero_or_masked()image, and as_mask_where_zero_or_masked()image.

Iterators

Every Polymath object can be used as an iterator, in which case it performs an iteration over the object's leading axis. Alternatively, ndenumerate()image iterates over every item in a multidimensional object.

Pickling

Because objects such as backplanes can be numerous and also quite large, we provide a variety of methods, both lossless and lossy, for compressing them during storage. As one example of optimization, only the un-masked elements of an object are stored; upon retrieval, all masked elements will have the value of the object's default attribute.

Arrays with integer elements are losslessly compressed using BZ2 compression. The numeric range is checked and values are stored using the fewest number of bytes sufficient to cover the range. Arrays with boolean elements are converted to bit arrays and then compressed using BZ2. These steps allow for extremely efficient data storage.

This module employs a variety of options for compressing floating point values.

  1. Very small arrays are stored using BZ2 compression.
  2. Constant arrays are stored as a single value plus a shape.
  3. Array values are divided by a specified constant and then stored as integers, using BZ2 compression, as described above.
  4. Arrays are compressed, with or without loss, using fpzip. This is a highly effective algorithm, especially for arrays such as backplanes that often exhibit smooth variations from pixel to pixel. See https://pypi.org/project/rms-fpzip.

For each object, the user can define the floating-point compression method using set_pickle_digits()image. One can also define the global default compression method using set_default_pickle_digits()image. The inputs to these functions are as follows:

digits (str, int, or float): The number of digits to preserve.

  • "double": preserve full precision using lossless fpzip compression.
  • "single": convert the array to single precision and then store it using lossless fpzip compression.
  • an number 7-16, defining the number of significant digits to preserve.

reference (str or float): How to interpret a numeric value of digits.

  • "fpzip": Use lossy fpzip compression, preserving the given number of digits.
  • a number: Preserve every number to the exact same absolute precision, scaling the number of digits by this value. For example, if digits=8 and reference=100, all values will be rounded to the nearest 1.e-6 before storage. This method uses option 3 above, where values are converted to integers for storage.

The remaining options for reference provide a variety of ways for its value to be generated automatically.

  • "smallest": Absolute accuracy will be 10**(-digits) times the non-zero array value closest to zero. This option guarantees that every value will preserve at least the requested number of digits. This is reasonable if you expect all values to fall within a similar dynamic range.
  • "largest": Absolute accuracy will be 10**(-digits) times the value in the array furthest from zero. This option is useful for arrays that contain a limited range of values, such as the components of a unit vector or angles that are known to fall between zero and 2*pi. In this case, it is probably not necessary to preserve the extra precision in values that just happen to fall very close zero.
  • "mean": Absolute accuracy will be 10**(-digits) times the mean of the absolute values in the array.
  • "median": Absolute accuracy will be 10**(-digits) times the median of the absolute values in the array. This is a good choice if a minority of values in the array are very different from the others, such as noise spikes or undefined geometry. In such a case, we want the precision to be based on the more "typical" values.
  • "logmean": Absolute accuracy will be 10**(-digits) times the log-mean of the absolute values in the array.

Contributing

Information on contributing to this package can be found in the Contributing Guide.

Links

Licensing

This code is licensed under the Apache License v2.0.

About

polymath Python module

Resources

License

Code of conduct

Contributing

Stars

Watchers

Forks

Packages

No packages published

Contributors 3

  •  
  •  
  •  

Languages