In the C++
engine
every variable is a matrix. In this simple situation where every
variable is a matrix, elementwise binary operations can be defined to
have very convenient properties. The problem is that these properties
are not standard in R
, probably because it is not the case
that all numeric variables are matrices. Differences between standard
R
mathematical functions and the McMasterPandemic
C++
engine make it more difficult to test the engine. This
document describes how to use R
so that elementwise binary
operators are comparable with those in the engine.
Consider the following three related matrices.
A = matrix(rnorm(6), 3, 2) # 3 by 2 matrix
x = matrix(rnorm(3)) # 3 by 1 matrix
y = t(rnorm(2)) # 1 by 2 matrix
They relate together in that A
and x
have
the same number of rows, and A
and y
have the
same number of columns. Note that although they are dimensionally
related, these three objects are of different shape in that
x
and y
have only one column and row
respectively, whereas A
has more than one row and
column.
Because of these relationships we might naturally want to multiply
every column of A
by the column vector x
, but
in R
we get the following error.
Here we define rigorously what convenient properties we expect of
elementwise binary operators when all variables are matrices, and show
how to convert elementwise binary operators in R
into
operators that have these properties.
Consider a generic binary operator, ⊗, that operates on two scalars to produce a third. We can overload this operator to take two matrices, x and y, and return a third matrix, z. z = x ⊗ y The elements of z are given by the following expression. $$ z_{i,j} = \cases{ \begin{array}{lll} x_{i,j} \otimes y_{i,j} & \text{if } n(x) = n(y) & \text{and } m(x) = m(y) & \text{Standard Hadamard product} \\ x_{i,j} \otimes y_{i,1} & \text{if } n(x) = n(y) & \text{and } m(y) = 1 & \text{Each matrix column times a column vector} \\ x_{i,j} \otimes y_{1,j} & \text{if } n(y) = 1 & \text{and } m(x) = m(y) & \text{Each matrix row times a row vector} \\ x_{i,1} \otimes y_{i,j} & \text{if } n(x) = n(y) & \text{and } m(x) = 1 & \text{Column vector times each matrix column} \\ x_{1,j} \otimes y_{i,j} & \text{if } n(x) = 1 & \text{and } m(x) = m(y) & \text{Row vector times each matrix row} \\ x_{1,1} \otimes y_{i,j} & \text{if } n(x) = m(x) = 1 & & \text{Scalar times a matrix, vector or scalar} \\ x_{i,j} \otimes y_{1,1} & \text{if } n(y) = m(y) = 1 & & \text{Matrix, vector or scalar times a scalar} \\ \end{array}} $$ Where the functions n() and m() give the numbers of rows and columns respectively.
We consider two matrix-valued operands, x
and
y
, and a standard binary operator, op
(e.g. +
), in R.
If the operands have the same shape then just do the operation.
This works because most R numeric operations are vectorized anyways.
If x
has either one row or one column, define the
operation with the arguments swapped otherwise keep the operator
unchanged.
Apply the base-R
sweep
function, making sure that the most matrix-like
operand comes first.
The BinaryOperator
constructor uses this algorithm.
Here are some examples.
(A = matrix(1:6, 3, 2))
#> [,1] [,2]
#> [1,] 1 4
#> [2,] 2 5
#> [3,] 3 6
(x = matrix(1:3, 3))
#> [,1]
#> [1,] 1
#> [2,] 2
#> [3,] 3
(y = matrix(1:2, 1))
#> [,1] [,2]
#> [1,] 1 2
times(A, x)
#> [,1] [,2]
#> [1,] 1 4
#> [2,] 4 10
#> [3,] 9 18
pow(A, y)
#> [,1] [,2]
#> [1,] 1 16
#> [2,] 2 25
#> [3,] 3 36
If we tried to do these operations naively, the R engine would complain.
try(A * x)
#> Error in A * x : non-conformable arrays
try(A ^ y)
#> Error in A^y : non-conformable arrays
Note that this algorithm does the right thing for both commutative
(e.g. *
) and non-commutative (e.g. ^
)
operators.