Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactoring and cleanup #9

Merged
merged 5 commits into from
Sep 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 100 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
[![PyPI python](https://img.shields.io/pypi/pyversions/rustynum)](https://pypi.org/project/rustynum)
![PyPI Version](https://badge.fury.io/py/rustynum.svg)
![License](https://img.shields.io/badge/license-MIT-blue.svg)

# RustyNum

RustyNum is a high-performance numerical computation library written in Rust, created to demonstrate the potential of Rust's SIMD (Single Instruction, Multiple Data) capabilities using the nightly `portable_simd` feature, and serving as a fast alternative to Numpy.
Expand All @@ -10,6 +14,10 @@ RustyNum is a high-performance numerical computation library written in Rust, cr

## Installation

Supported Python versions: `3.8`, `3.9`, `3.10`, `3.11`, `3.12`

Supported operating systems: `Windows x86`, `Linux x86`, `MacOS x86 & ARM`

### For Python

You can install RustyNum directly from PyPI:
Expand All @@ -18,46 +26,47 @@ You can install RustyNum directly from PyPI:
pip install rustynum
```

> If that does not work for you please create an issue with the operating system and Python version you're using!

## Quick Start Guide (Python)

If you're familiar with Numpy, you'll quickly get used to RustyNum!

```Python
import numpy as np
import rustynum as rnp

# Using Numpy
a = np.array([1.0, 2.0, 3.0, 4.0], dtype="float32")
a = a + 2
print(a.mean()) # 4.5

import rustynum as rnp
# Using RustyNum
b = rnp.NumArray([1.0, 2.0, 3.0, 4.0], dtype="float32")
b = b + 2

print(a.mean()) # 4.5
print(b.mean().item()) # 4.5
print(b.mean().item()) # 4.5
```

### Advanced Usage

You can perform advanced operations such as matrix-vector and matrix-matrix multiplications:

```Python

# matrix-vector dot product
# Matrix-vector dot product using Numpy
import numpy as np
import rustynum as rnp

a = np.random.rand(4 * 4).astype(np.float32)
b = np.random.rand(4).astype(np.float32)

result_numpy = np.dot(a.reshape((4, 4)), b)

import rustynum as rnp

# Matrix-vector dot product using RustyNum
a_rnp = rnp.NumArray(a.tolist())
b_rnp = rnp.NumArray(b.tolist())

result_rust = a_rnp.reshape([4, 4]).dot(b_rnp).tolist()

print(result_numpy) # [0.8383043 1.678406 1.4153088 0.7959367]
print(result_rust) # [0.8383043 1.678406 1.4153088 0.7959367]
print(result_numpy) # Example Output: [0.8383043, 1.678406, 1.4153088, 0.7959367]
print(result_rust) # Example Output: [0.8383043, 1.678406, 1.4153088, 0.7959367]
```

## Features
Expand All @@ -68,6 +77,8 @@ RustyNum offers a variety of numerical operations and data types, with more feat

- float64
- float32
- int32 (Planned)
- int64 (Planned)

### Supported Operations

Expand All @@ -87,13 +98,83 @@ RustyNum offers a variety of numerical operations and data types, with more feat
| Element-wise Mul | `a * b` | `a * b` |
| Element-wise Div | `a / b` | `a / b` |

### 1-Dimensional Arrays
### NumArray Class

Initialization

```Python
from rustynum import NumArray

# From a list
a = NumArray([1.0, 2.0, 3.0], dtype="float32")

# From another NumArray
b = NumArray(a)

# From nested lists (2D array)
c = NumArray([[1.0, 2.0], [3.0, 4.0]], dtype="float64")
```

Methods

`reshape(shape: List[int]) -> NumArray`

Reshapes the array to the specified shape.

```Python
reshaped = a.reshape([3, 1])
```

`matmul(other: NumArray) -> NumArray`

- Dot product
- Mean
- Min/Max
- Addition (+), Subtraction (-), Multiplication (\*), Division (/)
- Reshape
Performs matrix multiplication with another NumArray.

```Python
result = a.matmul(b)
# or
result = a @ b
```

`dot(other: NumArray) -> NumArray`

Computes the dot product with another NumArray.

```Python
dot_product = a.dot(b)
```

`mean(axes: Union[None, int, Sequence[int]] = None) -> Union[NumArray, float]`

Computes the mean along specified axes.

```Python
average = a.mean()
average_axis0 = a.mean(axes=0)
```

`min() -> float`

Returns the minimum value in the array.

```Python
minimum = a.min()
```

`max() -> float`

Returns the maximum value in the array.

```Python
maximum = a.max()
```

`tolist() -> Union[List[float], List[List[float]]]`

Converts the NumArray to a Python list.

```Python
list_representation = a.tolist()
```

### Multi-Dimensional Arrays

Expand All @@ -106,7 +187,7 @@ Planned Features:

- N-dimensional arrays
- Useful for filters, image processing, and machine learning
- Additional operations: arange, linspace, median, argmin, argmax, sort, std, var, zeros, cumsum, interp
- Additional operations: concat, exp, sigmoid, log, median, argmin, argmax, sort, std, var, zeros, cumsum, interp
- Integer support
- Extended shaping and reshaping capabilities
- C++ and WASM bindings
Expand Down
4 changes: 2 additions & 2 deletions bindings/python/benchmarks/test_performance_min.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ def setup_vector(dtype, size=1000):
# Function to perform min using rustynum
def min_rustynum(a, dtype):
a_rnp = rnp.NumArray(a, dtype=dtype)
return a_rnp.mean()
return a_rnp.min()


# Function to perform min using numpy
def min_numpy(a, dtype):
a_np = np.array(a, dtype=dtype)
return np.mean(a_np)
return np.min(a_np)


# Parametrized test function for different libraries, data types, and sizes
Expand Down
41 changes: 16 additions & 25 deletions bindings/python/rustynum/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,65 +19,56 @@ def __init__(
"""

if isinstance(data, NumArray):
self.inner = (
data.inner
) # Use the existing PyNumArray32 or PyNumArray64 object
self.dtype = data.dtype # Use the existing dtype
self.inner = data.inner
self.dtype = data.dtype
elif isinstance(data, (_rustynum.PyNumArray32, _rustynum.PyNumArray64)):
# Directly assign the Rust object if it's already a PyNumArray32 or PyNumArray64
self.inner = data
self.dtype = (
"float32" if isinstance(data, _rustynum.PyNumArray32) else "float64"
)
else:
# Determine if data is nested (e.g., list of lists)
if self._is_nested_list(data):
shape = self._infer_shape(data)
flat_data = self._flatten(data)
if not self._is_nested_list(data):
if dtype is None:
# Infer dtype from data types
dtype = self._infer_dtype_from_data(flat_data)
dtype = self._infer_dtype_from_data(data)
self.dtype = dtype

if dtype == "float32":
self.inner = _rustynum.PyNumArray32(flat_data, shape)
self.inner = _rustynum.PyNumArray32(data)
elif dtype == "float64":
self.inner = _rustynum.PyNumArray64(flat_data, shape)
self.inner = _rustynum.PyNumArray64(data)
else:
raise ValueError(f"Unsupported dtype: {dtype}")
else:
# Flat list
if dtype is None:
if all(isinstance(x, int) for x in data):
dtype = "int32" if all(x < 2**31 for x in data) else "int64"
elif all(isinstance(x, float) for x in data):
dtype = "float32" # Assume float32 for floating-point numbers
else:
raise ValueError("Data type could not be inferred from data.")
# Handling for nested lists
shape = self._infer_shape(data)
flat_data = self._flatten(data)

if dtype is None:
dtype = self._infer_dtype_from_data(flat_data)
self.dtype = dtype

if dtype == "float32":
self.inner = _rustynum.PyNumArray32(data)
self.inner = _rustynum.PyNumArray32(flat_data, shape)
elif dtype == "float64":
self.inner = _rustynum.PyNumArray64(data)
self.inner = _rustynum.PyNumArray64(flat_data, shape)
else:
raise ValueError(f"Unsupported dtype: {dtype}")

@staticmethod
def _is_nested_list(data: Any) -> bool:
"""
Determines if the provided data is a nested list (e.g., list of lists).
Determines if the provided data is likely a nested list by checking the first element.

Parameters:
data: The data to check.

Returns:
True if data is a nested list, False otherwise.
True if data is likely a nested list, False otherwise.
"""
if not isinstance(data, list):
return False
return any(isinstance(elem, list) for elem in data)
return len(data) > 0 and isinstance(data[0], list)

@staticmethod
def _infer_shape(data: List[Any]) -> List[int]:
Expand Down
9 changes: 8 additions & 1 deletion bindings/python/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,15 @@
classifiers=[
# Add classifiers to help users find your package
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Rust",
"Operating System :: OS Independent",
"Operating System :: MacOS :: MacOS X",
"Operating System :: POSIX :: Linux",
"Operating System :: Microsoft :: Windows",
"License :: OSI Approved :: MIT License",
],
python_requires=">=3.8",
Expand Down
Loading