Multiply MultiIndexSet Instances#

import minterpy as mp
import numpy as np

This guide demonstrates how to multiply instances of MultiIndexSet and shows the convention adopted by Minterpy regarding the product of two multi-indices.

The following three cases are considered:

  • MultiIndexSet instances with the same dimension

  • MultiIndexSet instances with different dimensions

  • MultiIndexSet instances with different \(l_p\)-degrees

The multiplication between MultiIndexSet instances may be carried out via a method call (MultiIndexSet.multiply()) or an operator (* or *=).

Instances with the same dimension#

As a motivating example, consider the following two multi-index sets of the same dimension:

\[\begin{align*} A & = \left\{ (0, 0), (1, 0), (2, 0), (0, 1) \right\}\\ B & = \left\{ (0, 0), (0, 1), (0, 2), (0, 3) \right\}\\ \end{align*}\]

Both sets are defined with respect to the same \(l_p\)-degree of \(1.0\). Notice that both sets are downward-closed.

mi_a = mp.MultiIndexSet(
    np.array([[0, 0], [1, 0], [2, 0], [0, 1]]),
    lp_degree=1.0,
)

mi_b = mp.MultiIndexSet(
    np.array([[0, 0], [0, 1], [0, 2], [0, 3]]),
    lp_degree=1.0,
)

The product of these two sets can be constructed using the multiplication (*) operator:

mi_prod_1 = mi_a * mi_b
print(mi_prod_1)
MultiIndexSet(m=2, n=5, p=1.0)
[[0 0]
 [1 0]
 [2 0]
 [0 1]
 [1 1]
 [2 1]
 [0 2]
 [1 2]
 [2 2]
 [0 3]
 [1 3]
 [2 3]
 [0 4]]

The dimension of the product set remains the same as that of the operands (in this case, \(2\)). Also notice that the product set is lexicographically sorted. Furthermore, because the operands are downward-closed, the resulting product remains downward-closed. That is:

mi_prod_1.is_downward_closed
True

The polynomial degree of the product set is:

print(mi_prod_1.poly_degree)
5

Note

If the \(l_p\)-degree of both operands is \(1.0\) then the polynomial degree of the product set is the sum of the polynomial degrees of the operands.

print((mi_a.poly_degree, mi_b.poly_degree))
(2, 3)

Alternatively, the union may also be created using a method call whose result is equivalent:

mi_prod_1 == mi_a.multiply(mi_b)
True

The multi-index sets do not have to be downward-closed for taking the product. Consider, for instance, a multi-index set that is not downward-closed:

\[\begin{split} C = \left\{ (1, 0), (0, 3) \right\}\\ \end{split}\]

again with respect to \(l_p\)-degree of \(1.0\).

mi_c = mp.MultiIndexSet(
    np.array([[1, 0], [0, 3]]),
    lp_degree=1.0,
)

The product with the multi-index set \(A\) is:

mi_a * mi_c
MultiIndexSet(
  array([[1, 0],
         [2, 0],
         [3, 0],
         [1, 1],
         [0, 3],
         [1, 3],
         [2, 3],
         [0, 4]]),
  lp_degree=1.0
)

Because one of the operands is not downward-closed, the product set is also not downward-closed. That is:

(mi_a * mi_c).is_downward_closed
False

Instances with different dimensions#

Multi-index sets of different dimension may also be multiplied. For instance, consider the following:

\[\begin{align*} D & = \left\{ (0, 0), (1, 0) \right\} & \text{dimension 2} \\ E & = \left\{ (0, 0, 0), (1, 0, 0), (0, 0, 1) \right\} & \text{dimension 3}\\ \end{align*}\]

Both sets are defined with the same \(l_p\)-degree of \(1.0\).

mi_d = mp.MultiIndexSet(
    np.array([[0, 0], [1, 0]]),
    lp_degree=1.0,
)
mi_e = mp.MultiIndexSet(
    np.array([[0, 0, 0], [1, 0, 0], [0, 0, 1]]),
    lp_degree=1.0,
)

The product set is:

mi_prod_2 = mi_d * mi_e
print(mi_prod_2)
MultiIndexSet(m=3, n=2, p=1.0)
[[0 0 0]
 [1 0 0]
 [2 0 0]
 [0 0 1]
 [1 0 1]]

Notice that the product set has the same dimension as the largest dimension of the operands (in this case, \(3\)).

The polynomial degree of the product set is:

print(mi_prod_2.poly_degree)
2

Instances with different \(l_p\)-degrees#

If two multi-index sets with different \(l_p\)-degrees are multiplied, then the \(l_p\)-degrees of the product set is the maximum of the \(l_p\)-degrees of the operands. Consider the following example:

\[\begin{align*} F & = \left\{ (0, 0), (1, 0), (0, 1), (1, 1) \right\} & \text{w.r.t } & p = \infty\\ G & = \left\{ (0, 0), (1, 0), (0, 1) \right\} & \text{w.r.t } & p = 1.0 \\ \end{align*}\]
mi_f = mp.MultiIndexSet(
    np.array([[0, 0], [1, 0], [1, 0], [1, 1]]),
    lp_degree=np.inf,
)
mi_g = mp.MultiIndexSet(
    np.array([[0, 0], [1, 0], [0, 1]]),
    lp_degree=1.0,
)

The product set is:

mi_prod_3 = mi_f * mi_g
print(mi_prod_3)
MultiIndexSet(m=2, n=2, p=inf)
[[0 0]
 [1 0]
 [2 0]
 [0 1]
 [1 1]
 [2 1]
 [1 2]]

By convention, the \(l_p\)-degree of the product set is the maximum of the \(l_p\)-degrees of the operands (in this case, \(\infty\)):

print(mi_prod_3.lp_degree)
inf

The polynomial degree of the product set is computed based on the resulting index set and \(l_p\)-degree:

print(mi_prod_3.poly_degree)
2

In-place multiplication#

The product of two MultiIndexSet instances may also be created in-place. That is, one of the instance is directly updated by the product.

To create the product via an in-place method call, set the inplace parameter to True (the default is set to False):

mi_a.multiply(mi_b, inplace=True)

The instance mi_a has been updated in-place:

mi_a == mi_prod_1
True

Alternatively, to create the product via an inplace operator:

mi_d *= mi_e

Similarly as before, the instance mi_d has been updated in-place:

mi_d == mi_prod_2
True