diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 000000000..ceff19d43 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,30 @@ +name: Publish to PyPI + +on: + push: + tags: + - 'v*' + workflow_dispatch: + +jobs: + build-and-publish: + runs-on: ubuntu-latest + permissions: + id-token: write + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: '3.10' + - name: Install dependencies + run: | + pip install build twine + - name: Build package + run: | + cd python + python -m build + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + password: ${{ secrets.PYPI_API_TOKEN }} + packages-dir: python/dist/ \ No newline at end of file diff --git a/.gitignore b/.gitignore index c31786a58..0aea4b567 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,10 @@ dataset* registry.txt # Python cache files -/**/__pycache__ \ No newline at end of file +/**/__pycache__ + +# Python build artifacts +dist/ +*.egg-info/ +__pycache__/ +*.pyc \ No newline at end of file diff --git a/python/pyspla/scalar.py b/python/pyspla/scalar.py index 4efd5d1cc..438337161 100644 --- a/python/pyspla/scalar.py +++ b/python/pyspla/scalar.py @@ -208,7 +208,7 @@ def __add__(self, other): return Scalar(dtype=self.dtype, value=self.get() + Scalar._value(other)) def __sub__(self, other): - return Scalar(dtype=self.dtype, value=self.get() + Scalar._value(other)) + return Scalar(dtype=self.dtype, value=self.get() - Scalar._value(other)) def __mul__(self, other): return Scalar(dtype=self.dtype, value=self.get() * Scalar._value(other)) diff --git a/python/pyspla/spla_x64.dll b/python/pyspla/spla_x64.dll new file mode 100644 index 000000000..4dcf7ba46 Binary files /dev/null and b/python/pyspla/spla_x64.dll differ diff --git a/python/tests/__init__.py b/python/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/python/tests/data/array_from_list/input.txt b/python/tests/data/array_from_list/input.txt new file mode 100644 index 000000000..a599a7a47 --- /dev/null +++ b/python/tests/data/array_from_list/input.txt @@ -0,0 +1,5 @@ +0 1 +1 2 +2 3 +3 4 +4 5 \ No newline at end of file diff --git a/python/tests/data/array_from_list/result.txt b/python/tests/data/array_from_list/result.txt new file mode 100644 index 000000000..a599a7a47 --- /dev/null +++ b/python/tests/data/array_from_list/result.txt @@ -0,0 +1,5 @@ +0 1 +1 2 +2 3 +3 4 +4 5 \ No newline at end of file diff --git a/python/tests/data/matrix_eadd/input_0.txt b/python/tests/data/matrix_eadd/input_0.txt new file mode 100644 index 000000000..f1fafddb7 --- /dev/null +++ b/python/tests/data/matrix_eadd/input_0.txt @@ -0,0 +1,3 @@ +0 0 1 +0 1 2 +1 1 3 \ No newline at end of file diff --git a/python/tests/data/matrix_eadd/input_1.txt b/python/tests/data/matrix_eadd/input_1.txt new file mode 100644 index 000000000..9a9ee1555 --- /dev/null +++ b/python/tests/data/matrix_eadd/input_1.txt @@ -0,0 +1,3 @@ +0 0 4 +1 0 5 +1 1 6 \ No newline at end of file diff --git a/python/tests/data/matrix_eadd/result.txt b/python/tests/data/matrix_eadd/result.txt new file mode 100644 index 000000000..ac6bb3ff7 --- /dev/null +++ b/python/tests/data/matrix_eadd/result.txt @@ -0,0 +1,4 @@ +0 0 5 +0 1 2 +1 0 5 +1 1 9 \ No newline at end of file diff --git a/python/tests/data/matrix_mxm/input_0.txt b/python/tests/data/matrix_mxm/input_0.txt new file mode 100644 index 000000000..f1fafddb7 --- /dev/null +++ b/python/tests/data/matrix_mxm/input_0.txt @@ -0,0 +1,3 @@ +0 0 1 +0 1 2 +1 1 3 \ No newline at end of file diff --git a/python/tests/data/matrix_mxm/input_1.txt b/python/tests/data/matrix_mxm/input_1.txt new file mode 100644 index 000000000..9a9ee1555 --- /dev/null +++ b/python/tests/data/matrix_mxm/input_1.txt @@ -0,0 +1,3 @@ +0 0 4 +1 0 5 +1 1 6 \ No newline at end of file diff --git a/python/tests/data/matrix_mxm/result.txt b/python/tests/data/matrix_mxm/result.txt new file mode 100644 index 000000000..1d17360f7 --- /dev/null +++ b/python/tests/data/matrix_mxm/result.txt @@ -0,0 +1,4 @@ +0 0 14 +0 1 12 +1 0 15 +1 1 18 \ No newline at end of file diff --git a/python/tests/data/matrix_reduce_by_row/input_0.txt b/python/tests/data/matrix_reduce_by_row/input_0.txt new file mode 100644 index 000000000..6a1d39265 --- /dev/null +++ b/python/tests/data/matrix_reduce_by_row/input_0.txt @@ -0,0 +1,4 @@ +0 0 1 +0 1 2 +1 1 3 +2 2 4 \ No newline at end of file diff --git a/python/tests/data/matrix_reduce_by_row/result.txt b/python/tests/data/matrix_reduce_by_row/result.txt new file mode 100644 index 000000000..c87925324 --- /dev/null +++ b/python/tests/data/matrix_reduce_by_row/result.txt @@ -0,0 +1,3 @@ +0 3 +1 3 +2 4 \ No newline at end of file diff --git a/python/tests/data/matrix_transpose/input.txt b/python/tests/data/matrix_transpose/input.txt new file mode 100644 index 000000000..35900ace8 --- /dev/null +++ b/python/tests/data/matrix_transpose/input.txt @@ -0,0 +1,3 @@ +0 1 1 +1 2 2 +2 0 3 \ No newline at end of file diff --git a/python/tests/data/matrix_transpose/result.txt b/python/tests/data/matrix_transpose/result.txt new file mode 100644 index 000000000..0ad576268 --- /dev/null +++ b/python/tests/data/matrix_transpose/result.txt @@ -0,0 +1,3 @@ +1 0 1 +2 1 2 +0 2 3 \ No newline at end of file diff --git a/python/tests/data/op_binary/input_0.txt b/python/tests/data/op_binary/input_0.txt new file mode 100644 index 000000000..73dbb21d6 --- /dev/null +++ b/python/tests/data/op_binary/input_0.txt @@ -0,0 +1,2 @@ +0 10 +1 20 \ No newline at end of file diff --git a/python/tests/data/op_binary/input_1.txt b/python/tests/data/op_binary/input_1.txt new file mode 100644 index 000000000..ecef37a2e --- /dev/null +++ b/python/tests/data/op_binary/input_1.txt @@ -0,0 +1,2 @@ +0 3 +1 5 \ No newline at end of file diff --git a/python/tests/data/op_binary/result_max.txt b/python/tests/data/op_binary/result_max.txt new file mode 100644 index 000000000..73dbb21d6 --- /dev/null +++ b/python/tests/data/op_binary/result_max.txt @@ -0,0 +1,2 @@ +0 10 +1 20 \ No newline at end of file diff --git a/python/tests/data/op_binary/result_mult.txt b/python/tests/data/op_binary/result_mult.txt new file mode 100644 index 000000000..a0e93ad36 --- /dev/null +++ b/python/tests/data/op_binary/result_mult.txt @@ -0,0 +1,2 @@ +0 30 +1 100 \ No newline at end of file diff --git a/python/tests/data/op_binary/result_plus.txt b/python/tests/data/op_binary/result_plus.txt new file mode 100644 index 000000000..86bcddfb9 --- /dev/null +++ b/python/tests/data/op_binary/result_plus.txt @@ -0,0 +1,2 @@ +0 13 +1 25 \ No newline at end of file diff --git a/python/tests/data/op_unary/input.txt b/python/tests/data/op_unary/input.txt new file mode 100644 index 000000000..bc037f1fb --- /dev/null +++ b/python/tests/data/op_unary/input.txt @@ -0,0 +1,3 @@ +0 5 +1 -3 +2 7 \ No newline at end of file diff --git a/python/tests/data/op_unary/result_abs.txt b/python/tests/data/op_unary/result_abs.txt new file mode 100644 index 000000000..92b2d06c6 --- /dev/null +++ b/python/tests/data/op_unary/result_abs.txt @@ -0,0 +1,3 @@ +0 5 +1 3 +2 7 \ No newline at end of file diff --git a/python/tests/data/op_unary/result_ainv.txt b/python/tests/data/op_unary/result_ainv.txt new file mode 100644 index 000000000..200487da9 --- /dev/null +++ b/python/tests/data/op_unary/result_ainv.txt @@ -0,0 +1,3 @@ +0 -5 +1 3 +2 -7 \ No newline at end of file diff --git a/python/tests/data/scalar_operations/input_0.txt b/python/tests/data/scalar_operations/input_0.txt new file mode 100644 index 000000000..9a037142a --- /dev/null +++ b/python/tests/data/scalar_operations/input_0.txt @@ -0,0 +1 @@ +10 \ No newline at end of file diff --git a/python/tests/data/scalar_operations/input_1.txt b/python/tests/data/scalar_operations/input_1.txt new file mode 100644 index 000000000..7813681f5 --- /dev/null +++ b/python/tests/data/scalar_operations/input_1.txt @@ -0,0 +1 @@ +5 \ No newline at end of file diff --git a/python/tests/data/scalar_operations/result_div.txt b/python/tests/data/scalar_operations/result_div.txt new file mode 100644 index 000000000..d8263ee98 --- /dev/null +++ b/python/tests/data/scalar_operations/result_div.txt @@ -0,0 +1 @@ +2 \ No newline at end of file diff --git a/python/tests/data/scalar_operations/result_mult.txt b/python/tests/data/scalar_operations/result_mult.txt new file mode 100644 index 000000000..c5b431b6c --- /dev/null +++ b/python/tests/data/scalar_operations/result_mult.txt @@ -0,0 +1 @@ +50 \ No newline at end of file diff --git a/python/tests/data/scalar_operations/result_plus.txt b/python/tests/data/scalar_operations/result_plus.txt new file mode 100644 index 000000000..3f10ffe7a --- /dev/null +++ b/python/tests/data/scalar_operations/result_plus.txt @@ -0,0 +1 @@ +15 \ No newline at end of file diff --git a/python/tests/data/scalar_operations/result_sub.txt b/python/tests/data/scalar_operations/result_sub.txt new file mode 100644 index 000000000..7813681f5 --- /dev/null +++ b/python/tests/data/scalar_operations/result_sub.txt @@ -0,0 +1 @@ +5 \ No newline at end of file diff --git a/python/tests/data/vector_add/input_0.txt b/python/tests/data/vector_add/input_0.txt new file mode 100644 index 000000000..28c52602e --- /dev/null +++ b/python/tests/data/vector_add/input_0.txt @@ -0,0 +1,4 @@ +0 5 +2 10 +3 15 +4 0 \ No newline at end of file diff --git a/python/tests/data/vector_add/input_1.txt b/python/tests/data/vector_add/input_1.txt new file mode 100644 index 000000000..a5db37918 --- /dev/null +++ b/python/tests/data/vector_add/input_1.txt @@ -0,0 +1,3 @@ +1 3 +2 7 +4 2 \ No newline at end of file diff --git a/python/tests/data/vector_add/result.txt b/python/tests/data/vector_add/result.txt new file mode 100644 index 000000000..67ce3a7bc --- /dev/null +++ b/python/tests/data/vector_add/result.txt @@ -0,0 +1,5 @@ +0 5 +1 3 +2 17 +3 15 +4 2 \ No newline at end of file diff --git a/python/tests/data/vector_from_list/input.txt b/python/tests/data/vector_from_list/input.txt new file mode 100644 index 000000000..fb8aeb5b9 --- /dev/null +++ b/python/tests/data/vector_from_list/input.txt @@ -0,0 +1,3 @@ +0 5 +2 10 +4 15 \ No newline at end of file diff --git a/python/tests/data/vector_from_list/result.txt b/python/tests/data/vector_from_list/result.txt new file mode 100644 index 000000000..82fcf3fe1 --- /dev/null +++ b/python/tests/data/vector_from_list/result.txt @@ -0,0 +1,5 @@ +0 5 +1 0 +2 10 +3 0 +4 15 \ No newline at end of file diff --git a/python/tests/test_array.py b/python/tests/test_array.py new file mode 100644 index 000000000..c1c4d3481 --- /dev/null +++ b/python/tests/test_array.py @@ -0,0 +1,39 @@ +import unittest +from pyspla import INT, Array + + +class TestArray(unittest.TestCase): + + def test_array_creation(self): + a = Array(INT, 5) + self.assertEqual(a.n_vals, 5) + self.assertEqual(a.shape, (5, 1)) + + def test_array_set_get(self): + a = Array(INT, 5) + a.set(0, 10) + a.set(2, 20) + a.set(4, 30) + + self.assertEqual(a.get(0), 10) + self.assertEqual(a.get(2), 20) + self.assertEqual(a.get(4), 30) + self.assertEqual(a.get(1), 0) + + def test_array_from_list(self): + values = [1, 2, 3, 4, 5] + a = Array.from_list(values, INT) + + self.assertEqual(a.n_vals, 5) + self.assertEqual(a.to_list(), values) + + def test_array_resize_clear(self): + a = Array.from_list([1, 2, 3], INT) + self.assertEqual(a.n_vals, 3) + + a.resize(5) + self.assertEqual(a.n_vals, 5) + + a.clear() + self.assertEqual(a.n_vals, 0) + self.assertTrue(a.empty) \ No newline at end of file diff --git a/python/tests/test_matrix.py b/python/tests/test_matrix.py new file mode 100644 index 000000000..8d596de88 --- /dev/null +++ b/python/tests/test_matrix.py @@ -0,0 +1,86 @@ +import unittest +from pathlib import Path + +from pyspla import INT, Matrix, Vector + + +def read_matrix_from_file(filepath): + I = [] + J = [] + V = [] + + with open(filepath, 'r') as f: + for line in f: + line = line.strip() + if line and not line.startswith('#'): + i, j, v = line.split() + I.append(int(i)) + J.append(int(j)) + V.append(int(v)) + + n_rows = max(I) + 1 if I else 3 + n_cols = max(J) + 1 if J else 3 + + return Matrix.from_lists(I, J, V, (n_rows, n_cols), INT) + + +class TestMatrix(unittest.TestCase): + + def test_matrix_creation(self): + M = Matrix((3, 4), INT) + self.assertEqual(M.n_rows, 3) + self.assertEqual(M.n_cols, 4) + self.assertEqual(M.shape, (3, 4)) + + def test_matrix_set_get(self): + M = Matrix((3, 3), INT) + M.set(0, 1, 5) + M.set(1, 2, 10) + M.set(2, 0, 15) + + self.assertEqual(M.get(0, 1), 5) + self.assertEqual(M.get(1, 2), 10) + self.assertEqual(M.get(2, 0), 15) + self.assertEqual(M.get(1, 1), 0) + + def test_matrix_eadd(self): + base_path = Path(__file__).parent / "data" / "matrix_eadd" + + A = read_matrix_from_file(base_path / "input_0.txt") + B = read_matrix_from_file(base_path / "input_1.txt") + expected = read_matrix_from_file(base_path / "result.txt") + + result = A.eadd(INT.PLUS, B) + self.assertEqual(result.to_lists(), expected.to_lists()) + + def test_matrix_transpose(self): + base_path = Path(__file__).parent / "data" / "matrix_transpose" + + M = read_matrix_from_file(base_path / "input.txt") + expected = read_matrix_from_file(base_path / "result.txt") + + result = M.transpose() + + for i in range(result.n_rows): + for j in range(result.n_cols): + self.assertEqual(result.get(i, j), expected.get(i, j)) + + def test_matrix_reduce_by_row(self): + base_path = Path(__file__).parent / "data" / "matrix_reduce_by_row" + + M = read_matrix_from_file(base_path / "input_0.txt") + + I_vec, V_vec = [], [] + with open(base_path / "result.txt", 'r') as f: + for line in f: + line = line.strip() + if line and not line.startswith('#'): + idx, val = line.split() + I_vec.append(int(idx)) + V_vec.append(int(val)) + + size = max(I_vec) + 1 if I_vec else 3 + expected = Vector.from_lists(I_vec, V_vec, size, INT) + + result = M.reduce_by_row(INT.PLUS) + self.assertEqual(result.to_lists(), expected.to_lists()) \ No newline at end of file diff --git a/python/tests/test_op.py b/python/tests/test_op.py new file mode 100644 index 000000000..6116e9c92 --- /dev/null +++ b/python/tests/test_op.py @@ -0,0 +1,67 @@ +import unittest +from pathlib import Path + +from pyspla import INT, Vector + + +def read_vector_from_file(filepath): + indices = [] + values = [] + + with open(filepath, 'r') as f: + for line in f: + line = line.strip() + if line and not line.startswith('#'): + idx, val = line.split() + indices.append(int(idx)) + values.append(int(val)) + + size = max(indices) + 1 if indices else 5 + return Vector.from_lists(indices, values, size, INT) + + +class TestOperations(unittest.TestCase): + + def test_unary_ops(self): + base_path = Path(__file__).parent / "data" / "op_unary" + + input_file = base_path / "input.txt" + abs_file = base_path / "result_abs.txt" + ainv_file = base_path / "result_ainv.txt" + + v = read_vector_from_file(input_file) + + result_abs = v.map(INT.ABS) + expected_abs = read_vector_from_file(abs_file) + self.assertEqual(result_abs.to_lists(), expected_abs.to_lists()) + + result_ainv = v.map(INT.AINV) + expected_ainv = read_vector_from_file(ainv_file) + self.assertEqual(result_ainv.to_lists(), expected_ainv.to_lists()) + + def test_binary_ops(self): + base_path = Path(__file__).parent / "data" / "op_binary" + + a_file = base_path / "input_0.txt" + b_file = base_path / "input_1.txt" + plus_file = base_path / "result_plus.txt" + mult_file = base_path / "result_mult.txt" + max_file = base_path / "result_max.txt" + + a = read_vector_from_file(a_file) + b = read_vector_from_file(b_file) + + result_plus = a.eadd(INT.PLUS, b) + expected_plus = read_vector_from_file(plus_file) + for idx in range(expected_plus.n_rows): + self.assertEqual(result_plus.get(idx), expected_plus.get(idx)) + + result_mult = a.emult(INT.MULT, b) + expected_mult = read_vector_from_file(mult_file) + for idx in range(expected_mult.n_rows): + self.assertEqual(result_mult.get(idx), expected_mult.get(idx)) + + result_max = a.eadd(INT.MAX, b) + expected_max = read_vector_from_file(max_file) + for idx in range(expected_max.n_rows): + self.assertEqual(result_max.get(idx), expected_max.get(idx)) \ No newline at end of file diff --git a/python/tests/test_scalar.py b/python/tests/test_scalar.py new file mode 100644 index 000000000..75a4217fc --- /dev/null +++ b/python/tests/test_scalar.py @@ -0,0 +1,51 @@ +import unittest +from pathlib import Path + +from pyspla import INT, FLOAT, Scalar + + +def read_scalar_from_file(filepath): + with open(filepath, 'r') as f: + line = f.readline().strip() + if '.' in line: + return float(line) + else: + return int(line) + + +class TestScalar(unittest.TestCase): + + def test_scalar_creation(self): + s = Scalar(INT, 10) + self.assertEqual(s.get(), 10) + + s = Scalar(FLOAT, 3.14) + self.assertAlmostEqual(s.get(), 3.14, places=5) + + def test_scalar_set_get(self): + s = Scalar(INT) + s.set(5) + self.assertEqual(s.get(), 5) + + s.set(42) + self.assertEqual(s.get(), 42) + + def test_scalar_operations(self): + base_path = Path(__file__).parent / "data" / "scalar_operations" + + a_file = base_path / "input_0.txt" + b_file = base_path / "input_1.txt" + plus_file = base_path / "result_plus.txt" + sub_file = base_path / "result_sub.txt" + mul_file = base_path / "result_mult.txt" + div_file = base_path / "result_div.txt" + + a_val = read_scalar_from_file(a_file) + b_val = read_scalar_from_file(b_file) + a = Scalar(INT, a_val) + b = Scalar(INT, b_val) + + self.assertEqual((a + b).get(), read_scalar_from_file(plus_file)) + self.assertEqual((a - b).get(), read_scalar_from_file(sub_file)) + self.assertEqual((a * b).get(), read_scalar_from_file(mul_file)) + self.assertEqual((a // b).get(), read_scalar_from_file(div_file)) \ No newline at end of file diff --git a/python/tests/test_vector.py b/python/tests/test_vector.py new file mode 100644 index 000000000..012ac48a7 --- /dev/null +++ b/python/tests/test_vector.py @@ -0,0 +1,64 @@ +import unittest +from pathlib import Path + +from pyspla import INT, Vector + + +def read_vector_from_file(filepath): + indices = [] + values = [] + + with open(filepath, 'r') as f: + for line in f: + line = line.strip() + if line and not line.startswith('#'): + idx, val = line.split() + indices.append(int(idx)) + values.append(int(val)) + + size = max(indices) + 1 if indices else 5 + return Vector.from_lists(indices, values, size, INT) + + +class TestVectorAdd(unittest.TestCase): + + def test_eadd(self): + base_path = Path(__file__).parent / "data" / "vector_add" + + a_file = base_path / "input_0.txt" + b_file = base_path / "input_1.txt" + result_file = base_path / "result.txt" + + a = read_vector_from_file(a_file) + b = read_vector_from_file(b_file) + expected = read_vector_from_file(result_file) + + result = a.eadd(INT.PLUS, b) + + for idx in range(expected.n_rows): + self.assertEqual(result.get(idx), expected.get(idx)) + + +class TestVectorFromList(unittest.TestCase): + + def test_from_list(self): + base_path = Path(__file__).parent / "data" / "vector_from_list" + + input_file = base_path / "input.txt" + result_file = base_path / "result.txt" + + indices, values = [], [] + with open(input_file, 'r') as f: + for line in f: + line = line.strip() + if line and not line.startswith('#'): + idx, val = line.split() + indices.append(int(idx)) + values.append(int(val)) + + size = max(indices) + 1 + v = Vector.from_lists(indices, values, size, INT) + expected = read_vector_from_file(result_file) + + for idx in range(expected.n_rows): + self.assertEqual(v.get(idx), expected.get(idx)) \ No newline at end of file