[MINOR] Python Left hand side ops

This commit add the left hand side ops to enable ops like:

3 > M
3 * M

furthermore this commit fixes a :bug: in transpose that did not set
shape correctly, and the same for matrix multiply.
diff --git a/src/main/python/systemds/operator/operation_node.py b/src/main/python/systemds/operator/operation_node.py
index 4d01a8b..e0206e6 100644
--- a/src/main/python/systemds/operator/operation_node.py
+++ b/src/main/python/systemds/operator/operation_node.py
@@ -180,38 +180,73 @@
     def __add__(self, other: VALID_ARITHMETIC_TYPES) -> 'OperationNode':
         return OperationNode(self.sds_context, '+', [self, other], shape=self.shape)
 
+    # Left hand side
+    def __radd__(self, other: VALID_ARITHMETIC_TYPES) -> 'OperationNode':
+        return OperationNode(self.sds_context, '+', [other, self], shape=self.shape)
+
     def __sub__(self, other: VALID_ARITHMETIC_TYPES) -> 'OperationNode':
         return OperationNode(self.sds_context, '-', [self, other], shape=self.shape)
 
+    # Left hand side
+    def __rsub__(self, other: VALID_ARITHMETIC_TYPES) -> 'OperationNode':
+        return OperationNode(self.sds_context, '-', [other, self], shape=self.shape)
+
     def __mul__(self, other: VALID_ARITHMETIC_TYPES) -> 'OperationNode':
         return OperationNode(self.sds_context, '*', [self, other], shape=self.shape)
 
+    def __rmul__(self, other: VALID_ARITHMETIC_TYPES) -> 'OperationNode':
+        return OperationNode(self.sds_context, '*', [other, self], shape=self.shape)
+
     def __truediv__(self, other: VALID_ARITHMETIC_TYPES) -> 'OperationNode':
         return OperationNode(self.sds_context, '/', [self, other], shape=self.shape)
 
+    def __rtruediv__(self, other: VALID_ARITHMETIC_TYPES) -> 'OperationNode':
+        return OperationNode(self.sds_context, '/', [other, self], shape=self.shape)
+
     def __floordiv__(self, other: VALID_ARITHMETIC_TYPES) -> 'OperationNode':
         return OperationNode(self.sds_context, '//', [self, other], shape=self.shape)
 
+    def __rfloordiv__(self, other: VALID_ARITHMETIC_TYPES) -> 'OperationNode':
+        return OperationNode(self.sds_context, '//', [other, self], shape=self.shape)
+
     def __lt__(self, other) -> 'OperationNode':
         return OperationNode(self.sds_context, '<', [self, other], shape=self.shape)
 
+    def __rlt__(self, other) -> 'OperationNode':
+        return OperationNode(self.sds_context, '<', [other, self], shape=self.shape)
+
     def __le__(self, other) -> 'OperationNode':
         return OperationNode(self.sds_context, '<=', [self, other], shape=self.shape)
 
+    def __rle__(self, other) -> 'OperationNode':
+        return OperationNode(self.sds_context, '<=', [other, self], shape=self.shape)
+
     def __gt__(self, other) -> 'OperationNode':
         return OperationNode(self.sds_context, '>', [self, other], shape=self.shape)
 
+    def __rgt__(self, other) -> 'OperationNode':
+        return OperationNode(self.sds_context, '>', [other, self], shape=self.shape)
+
     def __ge__(self, other) -> 'OperationNode':
         return OperationNode(self.sds_context, '>=', [self, other], shape=self.shape)
 
+    def __rge__(self, other) -> 'OperationNode':
+        return OperationNode(self.sds_context, '>=', [other, self], shape=self.shape)
+
     def __eq__(self, other) -> 'OperationNode':
         return OperationNode(self.sds_context, '==', [self, other], shape=self.shape)
 
+    def __req__(self, other) -> 'OperationNode':
+        return OperationNode(self.sds_context, '==', [other, self], shape=self.shape)
+
     def __ne__(self, other) -> 'OperationNode':
         return OperationNode(self.sds_context, '!=', [self, other], shape=self.shape)
 
+    def __rne__(self, other) -> 'OperationNode':
+        return OperationNode(self.sds_context, '!=', [other, self], shape=self.shape)
+
     def __matmul__(self, other: 'OperationNode') -> 'OperationNode':
-        return OperationNode(self.sds_context, '%*%', [self, other], shape=(self.shape[0], other.shape[0]))
+        return OperationNode(self.sds_context, '%*%', [self, other], shape=(self.shape[0], other.shape[1]))
 
     def sum(self, axis: int = None) -> 'OperationNode':
         """Calculate sum of matrix.
@@ -266,72 +301,72 @@
 
         :return: `OperationNode` representing operation
         """
-        return OperationNode(self.sds_context, 'abs', [self], shape = self.shape)
+        return OperationNode(self.sds_context, 'abs', [self], shape=self.shape)
 
     def sin(self) -> 'OperationNode':
         """Calculate sin.
 
         :return: `OperationNode` representing operation
         """
-        return OperationNode(self.sds_context, 'sin', [self], shape = self.shape)
+        return OperationNode(self.sds_context, 'sin', [self], shape=self.shape)
 
     def cos(self) -> 'OperationNode':
         """Calculate cos.
 
         :return: `OperationNode` representing operation
         """
-        return OperationNode(self.sds_context, 'cos', [self], shape = self.shape)
+        return OperationNode(self.sds_context, 'cos', [self], shape=self.shape)
 
     def tan(self) -> 'OperationNode':
         """Calculate tan.
 
         :return: `OperationNode` representing operation
         """
-        return OperationNode(self.sds_context, 'tan', [self], shape = self.shape)
+        return OperationNode(self.sds_context, 'tan', [self], shape=self.shape)
 
     def asin(self) -> 'OperationNode':
         """Calculate arcsin.
 
         :return: `OperationNode` representing operation
         """
-        return OperationNode(self.sds_context, 'asin', [self], shape = self.shape)
+        return OperationNode(self.sds_context, 'asin', [self], shape=self.shape)
 
     def acos(self) -> 'OperationNode':
         """Calculate arccos.
 
         :return: `OperationNode` representing operation
         """
-        return OperationNode(self.sds_context, 'acos', [self], shape = self.shape)
+        return OperationNode(self.sds_context, 'acos', [self], shape=self.shape)
 
     def atan(self) -> 'OperationNode':
         """Calculate arctan.
 
         :return: `OperationNode` representing operation
         """
-        return OperationNode(self.sds_context, 'atan', [self], shape = self.shape)
+        return OperationNode(self.sds_context, 'atan', [self], shape=self.shape)
 
     def sinh(self) -> 'OperationNode':
         """Calculate sin.
 
         :return: `OperationNode` representing operation
         """
-        return OperationNode(self.sds_context, 'sinh', [self], shape = self.shape)
+        return OperationNode(self.sds_context, 'sinh', [self], shape=self.shape)
 
     def cosh(self) -> 'OperationNode':
         """Calculate cos.
 
         :return: `OperationNode` representing operation
         """
-        return OperationNode(self.sds_context, 'cosh', [self], shape = self.shape)
+        return OperationNode(self.sds_context, 'cosh', [self], shape=self.shape)
 
     def tanh(self) -> 'OperationNode':
         """Calculate tan.
 
         :return: `OperationNode` representing operation
         """
-        return OperationNode(self.sds_context, 'tanh', [self], shape = self.shape)
+        return OperationNode(self.sds_context, 'tanh', [self], shape=self.shape)
 
-    def moment(self, moment, weights: DAGNode = None) -> 'OperationNode':
+    def moment(self, moment: int, weights: DAGNode = None) -> 'OperationNode':
         # TODO write tests
         self._check_matrix_op()
         unnamed_inputs = [self]
@@ -340,7 +375,7 @@
         unnamed_inputs.append(moment)
         return OperationNode(self.sds_context, 'moment', unnamed_inputs, output_type=OutputType.DOUBLE)
 
-    def write(self, destination: str, format:str = "binary", **kwargs: Dict[str, VALID_INPUT_TYPES]) -> 'OperationNode':
+    def write(self, destination: str, format: str = "binary", **kwargs: Dict[str, VALID_INPUT_TYPES]) -> 'OperationNode':
         """ Write input to disk. 
         The written format is easily read by SystemDSContext.read(). 
         There is no return on write.
@@ -350,22 +385,22 @@
         :param kwargs: Contains multiple extra specific arguments, can be seen at http://apache.github.io/systemds/site/dml-language-reference#readwrite-built-in-functions
         """
         unnamed_inputs = [self, f'"{destination}"']
-        named_parameters = {"format":f'"{format}"'}
+        named_parameters = {"format": f'"{format}"'}
         named_parameters.update(kwargs)
-        return OperationNode(self.sds_context, 'write', unnamed_inputs, named_parameters, output_type= OutputType.NONE)
+        return OperationNode(self.sds_context, 'write', unnamed_inputs, named_parameters, output_type=OutputType.NONE)
 
     def to_string(self, **kwargs: Dict[str, VALID_INPUT_TYPES]) -> 'OperationNode':
         """ Converts the input to a string representation.
         :return: `OperationNode` containing the string.
         """
-        return OperationNode(self.sds_context, 'toString', [self], kwargs, output_type= OutputType.SCALAR)
+        return OperationNode(self.sds_context, 'toString', [self], kwargs, output_type=OutputType.SCALAR)
 
     def print(self, **kwargs: Dict[str, VALID_INPUT_TYPES]) -> 'OperationNode':
         """ Prints the given Operation Node.
         There is no return on calling.
         To get the returned string look at the stdout of SystemDSContext.
         """
-        return OperationNode(self.sds_context, 'print', [self], kwargs, output_type= OutputType.NONE)
+        return OperationNode(self.sds_context, 'print', [self], kwargs, output_type=OutputType.NONE)
 
     def rev(self) -> 'OperationNode':
         """ Reverses the rows in a matrix
@@ -374,7 +409,7 @@
         """
 
         self._check_matrix_op()
-        return OperationNode(self.sds_context, 'rev', [self])
+        return OperationNode(self.sds_context, 'rev', [self], shape=self.shape)
 
     def order(self, by: int = 1, decreasing: bool = False,
               index_return: bool = False) -> 'OperationNode':
@@ -396,7 +431,7 @@
         named_input_nodes = {'target': self, 'by': by, 'decreasing': str(decreasing).upper(),
                              'index.return': str(index_return).upper()}
 
-        return OperationNode(self.sds_context, 'order', [], named_input_nodes=named_input_nodes)
+        return OperationNode(self.sds_context, 'order', [], named_input_nodes=named_input_nodes, shape=self.shape)
 
     def t(self) -> 'OperationNode':
         """ Transposes the input matrix
@@ -405,7 +440,11 @@
         """
 
         self._check_matrix_op()
-        return OperationNode(self.sds_context, 't', [self])
+        if(len(self.shape) > 1):
+            shape = (self.shape[1], self.shape[0])
+        else:
+            shape = (0, self.shape[0])
+        return OperationNode(self.sds_context, 't', [self], shape=shape)
 
     def cholesky(self, safe: bool = False) -> 'OperationNode':
         """ Computes the Cholesky decomposition of a symmetric, positive definite matrix
@@ -429,7 +468,7 @@
             if not np.allclose(self._np_array, self._np_array.transpose()):
                 raise ValueError("Matrix is not symmetric")
 
-        return OperationNode(self.sds_context, 'cholesky', [self])
+        return OperationNode(self.sds_context, 'cholesky', [self], shape=self.shape)
 
     def to_one_hot(self, num_classes: int) -> 'OperationNode':
         """ OneHot encode the matrix.
@@ -439,7 +478,7 @@
         :param num_classes: The number of classes to encode into. max value contained in the matrix must be <= num_classes
         :return: The OperationNode containing the oneHotEncoded values
         """
-        
+
         self._check_matrix_op()
         if len(self.shape) != 1:
             raise ValueError(
@@ -449,4 +488,4 @@
             raise ValueError("Number of classes should be larger than 1")
 
         named_input_nodes = {"X": self, "numClasses": num_classes}
-        return OperationNode(self.sds_context, 'toOneHot', named_input_nodes=named_input_nodes, shape=(self.shape[0], num_classes))
\ No newline at end of file
+        return OperationNode(self.sds_context, 'toOneHot', named_input_nodes=named_input_nodes, shape=(self.shape[0], num_classes))
diff --git a/src/main/python/tests/matrix/test_binary_op.py b/src/main/python/tests/matrix/test_binary_op.py
index 3f22c3b..d07d677 100644
--- a/src/main/python/tests/matrix/test_binary_op.py
+++ b/src/main/python/tests/matrix/test_binary_op.py
@@ -63,32 +63,58 @@
         self.assertTrue(np.allclose(
             (Matrix(self.sds, m1) / Matrix(self.sds, m2)).compute(), m1 / m2))
 
-    # TODO arithmetic with numpy rhs
-
-    # TODO arithmetic with numpy lhs
-
-    def test_plus3(self):
+    def test_plus3_rhs(self):
         self.assertTrue(np.allclose(
             (Matrix(self.sds, m1) + s).compute(), m1 + s))
 
-    def test_minus3(self):
+    def test_plus3_lhs(self):
+        self.assertTrue(np.allclose(
+            (s + Matrix(self.sds, m1) ).compute(), s + m1))
+
+    def test_minus3_rhs(self):
         self.assertTrue(np.allclose(
             (Matrix(self.sds, m1) - s).compute(), m1 - s))
 
-    def test_mul3(self):
+    def test_minus3_lhs(self):
+        self.assertTrue(np.allclose(
+            (s - Matrix(self.sds, m1)).compute(), s - m1 ))
+
+    def test_mul3_rhs(self):
         self.assertTrue(np.allclose(
             (Matrix(self.sds, m1) * s).compute(), m1 * s))
 
-    def test_div3(self):
+    def test_mul3_lhs(self):
+        self.assertTrue(np.allclose(
+            (s * Matrix(self.sds, m1)).compute(), s * m1))
+
+    def test_div3_rhs(self):
         self.assertTrue(np.allclose(
             (Matrix(self.sds, m1) / s).compute(), m1 / s))
 
+    def test_div3_lhs(self):
+        self.assertTrue(np.allclose(
+            (s / Matrix(self.sds, m1) ).compute(), s / m1))
+
     def test_matmul(self):
         self.assertTrue(np.allclose(
             (Matrix(self.sds, m1) @ Matrix(self.sds, m2)).compute(), m1.dot(m2)))
 
-    # TODO arithmetic with scala lhs
-
+    def test_matmul_chain(self):
+        m3 = np.ones((m2.shape[1], 10), dtype=np.uint8)
+        m = Matrix(self.sds, m1) @ Matrix(self.sds, m2) @ Matrix(
+            self.sds, m3)
+        res = (m).compute()
+        np_res = m1.dot(m2).dot(m3)
+        self.assertTrue(np.allclose(res, np_res))
+        self.assertTrue(np.allclose(m.shape, np_res.shape))
+    
+    def test_matmul_self(self):
+        m = Matrix(self.sds, m1).t() @ Matrix(self.sds, m1)
+        res = (m).compute()
+        np_res = np.transpose(m1).dot(m1)
+        self.assertTrue(np.allclose(res, np_res))
+        self.assertTrue(np.allclose(m.shape, np_res.shape))
+        
     def test_lt(self):
         self.assertTrue(np.allclose(
             (Matrix(self.sds, m1) < Matrix(self.sds, m2)).compute(), m1 < m2))
@@ -109,6 +135,25 @@
         self.assertTrue(np.allclose(
             Matrix(self.sds, m1).abs().compute(), np.abs(m1)))
 
+    def test_lt3_rhs(self):
+        self.assertTrue(np.allclose(
+            (Matrix(self.sds, m1) <3).compute(), m1 < 3))
+
+    def test_lt3_lhs(self):
+        self.assertTrue(np.allclose(
+            (3 < Matrix(self.sds, m1)).compute(), 3 < m1 ))
+
+    def test_gt3_rhs(self):
+        self.assertTrue(np.allclose(
+            (3 > Matrix(self.sds, m1)).compute(), 3 > m1 ))
+
+    def test_le3_rhs(self):
+        self.assertTrue(np.allclose(
+            (3<= Matrix(self.sds, m1) ).compute(), 3 <= m1 ))
+
+    def test_ge3_rhs(self):
+        self.assertTrue(np.allclose(
+            (3 >= Matrix(self.sds, m1)).compute(), 3>= m1))
 
 if __name__ == "__main__":
     unittest.main(exit=False)