# Computing Sum of Element-Wise Product

NumPy provides powerful tools for performing mathematical operations on arrays and matrices. One of the most common operations performed on matrices is matrix multiplication. In some cases, we may need to compute the product of several matrices at once. For example, suppose we have `p` matrices `A1, A2, ..., Ap`, and we want to compute their product `A1*A2*...*Ap`. One way to do this is to perform `p-1` matrix multiplications, each time multiplying the current result with the next matrix. However, this can be computationally expensive, especially when dealing with large matrices. Fortunately, NumPy provides multiple functionalities to do this efficiently, which are as follows:

#### 1. Using "np.einsum()":

• The `np.ones()` is a function that returns a new array of a given shape and data type, where all the elements are set to 1. In this example code, a 3-dimensional array `M` of shape `(p, n, n)` and a 3-dimensional array `V` of shape `(p, n, 1)` is created using the `np.ones()` function.
• The `np.einsum()` function implements Einstein summation notation, which is a compact way to express tensor operations.
• The `subscripts` argument `'pjk,pki->ij'` of `np.einsum()` specifies the indices of the input arrays to be contracted and the output array and the function performs a tensor contraction over the indices `p`, `k`, and `i` of `M` and `p`, `k`, and `j` of `V`, resulting in a 2-dimensional array of shape `(n, n)` which is assigned to the variable `S`.
• Finally, the code prints the value of `S` to the console.
• The output of the code will be a 2D array of shape `(n, n)`, with all elements set to the value `p * n`:
• The advantage of using NumPy’s broadcasting rules to create this array is that it can be done with a single line of code, without the need for loops or list comprehensions. This can be much faster and more memory-efficient than equivalent operations using loops or list comprehensions, especially for large arrays.
• Also, NumPy’s broadcasting rules make it easy to perform element-wise operations between arrays of different shapes and sizes, which is a common task in scientific computing and data analysis.

#### 2. Using "np.dot()":

• In this method too, the arrays `M` and `V` are created in the same way using the `np.ones()` function.
• The `dot()` function performs matrix multiplication between two arrays. Here, the multiplication is performed between `M` and `V`. Since `V` has shape `(p, n, 1)`, the result of the dot product will be a 3D array of shape `(p, n, 1)`.
• The `squeeze()` function is used to remove any dimensions with size 1 from an array. In this case, it is used to remove the extra dimension of size 1 from the result of the dot product, since we want a 2D array of shape `(p, n)` instead of a 3D array of shape `(p, n, 1)`.
• Lastly, the `sum()` function is used to compute the sum of all elements in `S` along the first dimension (`p`). This results in a 1D array of shape `(n,)`, which is stored in `S`.
• The output of this code will be a 1D array of shape `(n,)`, with all elements set to the value `p * n`:
• The advantage of using matrix multiplication and broadcasting rules to perform this operation is that it can be done with a single line of code, without the need for loops or list comprehensions. This can be much faster and more memory-efficient than equivalent operations using loops or list comprehensions, especially for large arrays.