Introduction

In numpy the concept of array is generalized to imply arrays of arbitrary dimension, overlapping with the concept of scalars, matrices and tensors. To allow arrays of various dimensions to operate together it defines unambiguous broadcasting rules for what to expect. The results are a library that is used as the reference for almost all the numerical Python community.

In mathematical literature the term polynomial expansions is used to denote a collection of polynomials. Though they strictly do not need to, they are often indexed, giving each polynomial both a label and a position for where to locate a polynomial relative to the others. Assuming that there always is an index, one could say that polynomial expansions could just as well be termed polynomial array. And using the rules defined in numpy, there is no reason not to also start talking about multidimensional polynomial arrays.

The main idea here is that in the same way as numpy.ndarray are composed of scalars, chaospy.ndpoly – the baseclass for the polynomial arrays – are composed of simpler polynomials. This gives us a mental model of a polynomial that looks like this:

\[\Phi(q_1, \dots, q_D) = [\Phi_1(q_1, \dots, q_D), \cdots, \Phi_N(q_1, \dots, q_D)]\]

where \(\Phi\) is polynomial vector, \(N\) is the number of terms in the polynomial sum, and \(q_d\) is the \(d\)-th indeterminant name. This mental model is shown in practice in how chaospy displays its polynomials in the REPL:

>>> q0, q1 = chaospy.variable(2)
>>> expansion = chaospy.polynomial([1, q0, q1**2])
>>> expansion
polynomial([1, q0, q1**2])

Here chaospy.variable() creates to simple indeterminants, and the chaospy.polynomial() constructor joins an array of polynomials into a polynomial array, much like numpy.array() does for numeric in numpy.

Another way to look at the polynomials is to keep the polynomial array as a single polynomial sum: A multivariate polynomial can in the case of chaospy be defined as:

\[\Phi(q_1, \dots, q_D) = \sum_{n=1}^N c_n q_1^{k_{1n}} \cdots q_D^{k_{Dn}}\]

where \(c_n\) is a multidimensional polynomial coefficients, and \(k_{nd}\) is the exponent for the \(n\)-th polynomial term and the \(d\)-th indeterminant name.

Neither of the two ways of representing a polynomial array is incorrect, and serves different purposes. The former works well for visualization, while the latter form gives a better mental model of how chaospy handles its polynomial internally.

Modeling polynomials by storing the coefficients as multidimensional arrays is deliberate. Assuming few \(k_{nd}\) and large dimensional \(c_n\), all numerical operations that are limited to the coefficients, can be done fast, as numpy can do the heavy lifting.

This way of representing a polynomial also means that to uniquely defined a polynomial, we only need the three components:

  • coefficients – the polynomial coefficients \(c_n\) as multidimensional arrays.

  • exponents – the exponents \(k_{nd}\) as a 2-dimensional matrix.

  • indeterminants – the names of the variables, typically q0, q1, etc.

We can access these three defining properties directly from any chaospy.ndpoly polynomial. For example, for a simple polynomial with scalar coefficients:

>>> q0, q1 = chaospy.variable(2)
>>> poly = chaospy.polynomial(4*q0+3*q1-1)
>>> poly
polynomial(3*q1+4*q0-1)
>>> indet = poly.indeterminants
>>> indet
polynomial([q0, q1])
>>> coeff = poly.coefficients
>>> coeff
[4, 3, -1]
>>> expon = poly.exponents
>>> expon
array([[1, 0],
       [0, 1],
       [0, 0]], dtype=uint32)

Because these three properties uniquely define a polynomial array, they can also be used to reconstruct the original polynomial:

>>> terms = coeff*chaospy.prod(indet**expon, axis=-1)
>>> terms
polynomial([4*q0, 3*q1, -1])
>>> poly = chaospy.sum(terms, axis=0)
>>> poly
polynomial(3*q1+4*q0-1)

Here chaospy.prod() and chaospy.sum() is used analogous to their numpy counterparts numpy.prod() and numpy.sum() to multiply and add terms together over an axis. See Numpy functions for more details on how this works.

Note

As mentioned the chosen representation works best with relatively few \(k_{nd}\) and large \(c_n\). For large number \(k_{nd}\) and relatively small \(c_n\) however, the advantage disappears. And even worse, in the case where polynomial terms \(q_1^{k_{1n}} \cdots q_D^{k_{Dn}}\) are sparsely represented, the chaospy representation is quite memory inefficient. So it is worth keeping in mind that the advantage of this implementation depends a little upon what kind of problems you are working on. It is not the tool for all problems.