[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)