mirror of
https://github.com/kashifulhaque/smoltorch.git
synced 2025-12-05 22:52:50 +00:00
nanotorch implementation
This commit is contained in:
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"python.REPL.enableREPLSmartSend": false
|
||||
}
|
||||
28
README.md
28
README.md
@@ -0,0 +1,28 @@
|
||||
|
||||
|
||||
|
||||
|
||||
Let's tidy up any small things
|
||||
|
||||
|
||||
|
||||
Write a really nice README on what it does, how it does and how can one use it
|
||||
|
||||
|
||||
|
||||
Make it ready for release (will push to pypi)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Make a GitHub action for this actually, so I don't have to do this manually
|
||||
|
||||
|
||||
|
||||
There's already a pkg on pypi named "nanotorch", so we might need to name it something else
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
61
examples/train_regression.py
Normal file
61
examples/train_regression.py
Normal file
@@ -0,0 +1,61 @@
|
||||
from sklearn.datasets import make_regression
|
||||
from sklearn.model_selection import train_test_split
|
||||
from nanotorch.tensor import Tensor
|
||||
from nanotorch.nn import MLP, SGD
|
||||
|
||||
# Generate synthetic regression data
|
||||
print("Generating data...")
|
||||
X, y = make_regression(n_samples=200, n_features=5, noise=10, random_state=42)
|
||||
y = y.reshape(-1, 1) # Make y 2D: (200, 1)
|
||||
|
||||
# Split into train/test
|
||||
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
|
||||
|
||||
print(f"Training samples: {X_train.shape[0]}")
|
||||
print(f"Test samples: {X_test.shape[0]}")
|
||||
print(f"Features: {X_train.shape[1]}")
|
||||
|
||||
# Create model
|
||||
model = MLP([5, 16, 16, 1]) # 5 inputs -> 16 -> 16 -> 1 output
|
||||
optimizer = SGD(model.parameters(), lr=0.001)
|
||||
|
||||
# Training loop
|
||||
epochs = 100
|
||||
print("\nTraining...")
|
||||
|
||||
for epoch in range(epochs):
|
||||
# Convert to tensors
|
||||
X_tensor = Tensor(X_train)
|
||||
y_tensor = Tensor(y_train)
|
||||
|
||||
# Forward pass
|
||||
y_pred = model(X_tensor)
|
||||
|
||||
# MSE loss
|
||||
loss = ((y_pred - y_tensor) ** 2).mean()
|
||||
|
||||
# Backward pass
|
||||
optimizer.zero_grad()
|
||||
loss.backward()
|
||||
|
||||
# Update weights
|
||||
optimizer.step()
|
||||
|
||||
# Print progress
|
||||
if (epoch + 1) % 10 == 0:
|
||||
print(f"Epoch {epoch + 1}/{epochs}, Loss: {loss.data:.4f}")
|
||||
|
||||
# Evaluate on test set
|
||||
print("\nEvaluating on test set...")
|
||||
X_test_tensor = Tensor(X_test)
|
||||
y_test_tensor = Tensor(y_test)
|
||||
|
||||
y_pred_test = model(X_test_tensor)
|
||||
test_loss = ((y_pred_test - y_test_tensor) ** 2).mean()
|
||||
|
||||
print(f"Test Loss (MSE): {test_loss.data:.4f}")
|
||||
|
||||
# Show some predictions
|
||||
print("\nSample predictions:")
|
||||
for i in range(5):
|
||||
print(f"True: {y_test[i, 0]:.2f}, Predicted: {y_pred_test.data[i, 0]:.2f}")
|
||||
@@ -101,4 +101,4 @@ class Value:
|
||||
|
||||
self.grad = 1.0
|
||||
for node in reversed(topo):
|
||||
node._backward()
|
||||
node._backward()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import random
|
||||
from micrograd.engine import Value
|
||||
from engine import Value
|
||||
|
||||
class Neuron:
|
||||
def __init__(self, n_inputs: int):
|
||||
|
||||
0
nanotorch/__init__.py
Normal file
0
nanotorch/__init__.py
Normal file
88
nanotorch/nn.py
Normal file
88
nanotorch/nn.py
Normal file
@@ -0,0 +1,88 @@
|
||||
import numpy as np
|
||||
from nanotorch.tensor import Tensor
|
||||
|
||||
class Linear:
|
||||
def __init__(self, in_features, out_features):
|
||||
"""
|
||||
A linear layer: y = x @ W + b
|
||||
|
||||
Args:
|
||||
in_features: input dims
|
||||
out_features: output dims
|
||||
"""
|
||||
self.W = Tensor(np.random.randn(in_features, out_features) * 0.1)
|
||||
self.b = Tensor(np.zeros(out_features))
|
||||
|
||||
def __call__(self, x):
|
||||
"""
|
||||
Forward pass: y = x @ W + b
|
||||
|
||||
Args:
|
||||
x: input tensor, shape = (batch_size, in_features)
|
||||
|
||||
Returns:
|
||||
output tensor, shape = (batch_size, out_features)
|
||||
"""
|
||||
return x @ self.W + self.b
|
||||
|
||||
def parameters(self):
|
||||
"""
|
||||
Returns:
|
||||
List of trainable parameters
|
||||
"""
|
||||
return [self.W, self.b]
|
||||
|
||||
class MLP:
|
||||
"""
|
||||
An MLP is just stacked linear layers with activations: Input → Linear → ReLU → Linear → ReLU → Linear → Output
|
||||
"""
|
||||
def __init__(self, layer_sizes):
|
||||
"""
|
||||
MLP with ReLU activation
|
||||
|
||||
Args:
|
||||
layer_sizes: list of layer dims [input, hidden1, hidden2, ..., output]
|
||||
e.g. [2, 16, 16, 1] means:
|
||||
- input: 2 features
|
||||
- 2 hidden layers with 16 neurons each
|
||||
- output: 1 value
|
||||
"""
|
||||
self.layers = []
|
||||
for i in range(len(layer_sizes) - 1):
|
||||
self.layers.append(Linear(layer_sizes[i], layer_sizes[i + 1]))
|
||||
|
||||
def __call__(self, x):
|
||||
"""
|
||||
Forward pass with ReLU activation between layers.
|
||||
No activation on the final layer (common for regression/raw logits).
|
||||
"""
|
||||
for i, layer in enumerate(self.layers):
|
||||
x = layer(x)
|
||||
if i < len(self.layers) - 1:
|
||||
x = x.relu()
|
||||
|
||||
return x
|
||||
|
||||
def parameters(self):
|
||||
params = []
|
||||
for layer in self.layers:
|
||||
params.extend(layer.parameters())
|
||||
return params
|
||||
|
||||
class SGD:
|
||||
def __init__(self, parameters, lr=0.01):
|
||||
"""
|
||||
Args
|
||||
parameters: list of Tensor objects to minimize
|
||||
lr: learning rate
|
||||
"""
|
||||
self.parameters = parameters
|
||||
self.lr = lr
|
||||
|
||||
def step(self):
|
||||
for param in self.parameters:
|
||||
param.data -= self.lr * param.grad
|
||||
|
||||
def zero_grad(self):
|
||||
for param in self.parameters:
|
||||
param.grad = np.zeros_like(param.data, dtype=np.float64)
|
||||
189
nanotorch/tensor.py
Normal file
189
nanotorch/tensor.py
Normal file
@@ -0,0 +1,189 @@
|
||||
import numpy as np
|
||||
|
||||
class Tensor:
|
||||
def __init__(self, data, _parents=(), _op=''):
|
||||
self.data = np.array(data) if not isinstance(data, np.ndarray) else data
|
||||
self._parents = _parents
|
||||
self._op = _op
|
||||
|
||||
# gradient: same shape as data, init to zeros
|
||||
self.grad = np.zeros_like(self.data, dtype=np.float64)
|
||||
self._backward = lambda: None
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Tensor(data={self.data}, grad={self.grad})"
|
||||
|
||||
def __neg__(self) -> 'Tensor':
|
||||
return self * -1
|
||||
|
||||
def __sub__(self, other: 'Tensor') -> 'Tensor':
|
||||
return self + (-other)
|
||||
|
||||
def __rsub__(self, other: 'Tensor') -> 'Tensor':
|
||||
return Tensor(other) - self
|
||||
|
||||
def __rmul__(self, other: 'Tensor') -> 'Tensor':
|
||||
return self * other
|
||||
|
||||
def __radd__(self, other: 'Tensor') -> 'Tensor':
|
||||
return self + other
|
||||
|
||||
def _unbroadcast(self, grad, original_shape):
|
||||
# sum over leading dimensions that were added
|
||||
while len(grad.shape) > len(original_shape):
|
||||
grad = grad.sum(axis=0)
|
||||
|
||||
# sum over dims that were size 1 but got broadcasted
|
||||
for i, (grad_dim, orig_dim) in enumerate(zip(grad.shape, original_shape)):
|
||||
if orig_dim == 1 and grad_dim > 1:
|
||||
grad = grad.sum(axis=i, keepdims=True)
|
||||
|
||||
return grad
|
||||
|
||||
def __add__(self, other: 'Tensor') -> 'Tensor':
|
||||
# handle scalar addition
|
||||
other = other if isinstance(other, Tensor) else Tensor(other)
|
||||
out = Tensor(self.data + other.data, (self, other), '+')
|
||||
|
||||
def _backward():
|
||||
# undo broadcast by summing over broadcast dims
|
||||
grad_self = self._unbroadcast(out.grad, self.data.shape)
|
||||
grad_other = other._unbroadcast(out.grad, other.data.shape)
|
||||
|
||||
# accumulate grads
|
||||
self.grad += grad_self
|
||||
other.grad += grad_other
|
||||
|
||||
out._backward = _backward
|
||||
return out
|
||||
|
||||
def __mul__(self, other: 'Tensor') -> 'Tensor':
|
||||
other = other if isinstance(other, Tensor) else Tensor(other)
|
||||
out = Tensor(self.data * other.data, (self, other), '*')
|
||||
|
||||
def _backward():
|
||||
# local gradients with broadcasting
|
||||
grad_self = out.grad * other.data
|
||||
grad_other = out.grad * self.data
|
||||
grad_self = self._unbroadcast(grad_self, self.data.shape)
|
||||
grad_other = other._unbroadcast(grad_other, other.data.shape)
|
||||
|
||||
# accumulate grads
|
||||
self.grad += grad_self
|
||||
other.grad += grad_other
|
||||
|
||||
out._backward = _backward
|
||||
return out
|
||||
|
||||
def __matmul__(self, other: 'Tensor') -> 'Tensor':
|
||||
other = other if isinstance(other, Tensor) else Tensor(other)
|
||||
out = Tensor(self.data @ other.data, (self, other), '@')
|
||||
|
||||
def _backward():
|
||||
self_data = self.data
|
||||
other_data = other.data
|
||||
|
||||
if other_data.ndim == 1:
|
||||
grad_self = out.grad.reshape(-1, 1) @ other_data.reshape(1, -1)
|
||||
else:
|
||||
other_data_T = other_data.swapaxes(-2, -1)
|
||||
grad_self = out.grad @ other_data_T
|
||||
grad_self = self._unbroadcast(grad_self, self_data.shape)
|
||||
|
||||
if self_data.ndim == 1:
|
||||
self_data_T = self_data.reshape(-1, 1)
|
||||
grad_other = self_data_T @ out.grad.reshape(1, -1)
|
||||
else:
|
||||
self_data_T = self_data.swapaxes(-2, -1)
|
||||
grad_other = self_data_T @ out.grad
|
||||
grad_other = self._unbroadcast(grad_other, other_data.shape)
|
||||
|
||||
# accumulate grads
|
||||
self.grad += grad_self
|
||||
other.grad += grad_other
|
||||
|
||||
out._backward = _backward
|
||||
return out
|
||||
|
||||
def __pow__(self, power) -> 'Tensor':
|
||||
assert isinstance(power, (int, float)), "only support int/float powers"
|
||||
|
||||
out = Tensor(self.data ** power, (self, ), f'**{power}')
|
||||
|
||||
def _backward():
|
||||
self.grad += power * (self.data ** (power - 1)) * out.grad
|
||||
|
||||
out._backward = _backward
|
||||
return out
|
||||
|
||||
def sum(self, axis=None, keepdims=False) -> 'Tensor':
|
||||
out = Tensor(self.data.sum(axis=axis, keepdims=keepdims), (self, ), 'sum')
|
||||
|
||||
def _backward():
|
||||
grad = out.grad
|
||||
if axis is not None and not keepdims:
|
||||
grad = np.expand_dims(grad, axis=axis)
|
||||
|
||||
self.grad += np.broadcast_to(grad, self.data.shape)
|
||||
|
||||
out._backward = _backward
|
||||
return out
|
||||
|
||||
def mean(self, axis=None, keepdims=False):
|
||||
if axis is None:
|
||||
count = self.data.size
|
||||
else:
|
||||
count = self.data.shape[axis]
|
||||
|
||||
out = Tensor(self.data.mean(axis=axis, keepdims=keepdims), (self, ), 'mean')
|
||||
|
||||
def _backward():
|
||||
grad = out.grad / count
|
||||
if axis is not None and not keepdims:
|
||||
grad = np.expand_dims(grad, axis=axis)
|
||||
|
||||
self.grad += np.broadcast_to(grad, self.data.shape)
|
||||
|
||||
out._backward = _backward
|
||||
return out
|
||||
|
||||
def backward(self):
|
||||
# build topological order
|
||||
topo = []
|
||||
visited = set()
|
||||
|
||||
def build_topo(tensor):
|
||||
if tensor not in visited:
|
||||
visited.add(tensor)
|
||||
for parent in tensor._parents:
|
||||
build_topo(parent)
|
||||
topo.append(tensor)
|
||||
|
||||
build_topo(self)
|
||||
|
||||
# init gradient of output to 1
|
||||
self.grad = np.ones_like(self.data, dtype=np.float64)
|
||||
|
||||
# backprop
|
||||
for node in reversed(topo):
|
||||
node._backward()
|
||||
|
||||
# activation functions
|
||||
def relu(self) -> 'Tensor':
|
||||
out = Tensor(np.maximum(0, self.data), (self, ), 'ReLU')
|
||||
|
||||
def _backward():
|
||||
self.grad += (self.data > 0) * out.grad
|
||||
|
||||
out._backward = _backward
|
||||
return out
|
||||
|
||||
def tanh(self) -> 'Tensor':
|
||||
t = np.tanh(self.data)
|
||||
out = Tensor(t, (self, ), 'tanh')
|
||||
|
||||
def _backward():
|
||||
self.grad += (1 - t**2) * out.grad
|
||||
|
||||
out._backward = _backward
|
||||
return out
|
||||
@@ -10,4 +10,14 @@ dependencies = [
|
||||
"matplotlib>=3.10.7",
|
||||
"mlx>=0.29.4",
|
||||
"nbconvert>=7.16.6",
|
||||
"numpy>=2.3.4",
|
||||
"scikit-learn>=1.7.2",
|
||||
]
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"pytest>=9.0.1",
|
||||
]
|
||||
|
||||
[tool.setuptools]
|
||||
packages = ["nanotorch", "micrograd"]
|
||||
|
||||
37
tests/test_activations.py
Normal file
37
tests/test_activations.py
Normal file
@@ -0,0 +1,37 @@
|
||||
from nanotorch.tensor import Tensor
|
||||
|
||||
# Test 1: ReLU
|
||||
print("Test 1 - ReLU:")
|
||||
x = Tensor([-2.0, -1.0, 0.0, 1.0, 2.0])
|
||||
y = x.relu()
|
||||
|
||||
y.backward()
|
||||
|
||||
print(f"x.data: {x.data}")
|
||||
print(f"y.data: {y.data}") # Should be [0, 0, 0, 1, 2]
|
||||
print(f"x.grad: {x.grad}") # Should be [0, 0, 0, 1, 1]
|
||||
|
||||
# Test 2: Tanh
|
||||
print("\nTest 2 - Tanh:")
|
||||
x = Tensor([0.0, 1.0, 2.0])
|
||||
y = x.tanh()
|
||||
|
||||
y.backward()
|
||||
|
||||
print(f"x.data: {x.data}")
|
||||
print(f"y.data: {y.data}") # Should be [0, 0.76, 0.96] approx
|
||||
print(f"x.grad: {x.grad}") # Should be [1, 0.42, 0.07] approx (1 - tanh²)
|
||||
|
||||
# Test 3: ReLU in a computation graph
|
||||
print("\nTest 3 - ReLU in computation:")
|
||||
x = Tensor([[-1.0, 2.0],
|
||||
[3.0, -4.0]])
|
||||
w = Tensor([[0.5, 0.5],
|
||||
[0.5, 0.5]])
|
||||
z = (x @ w).relu() # Linear layer + ReLU
|
||||
|
||||
z.backward()
|
||||
|
||||
print(f"z.data:\n{z.data}")
|
||||
print(f"x.grad:\n{x.grad}")
|
||||
print(f"w.grad:\n{w.grad}")
|
||||
23
tests/test_add.py
Normal file
23
tests/test_add.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from nanotorch.tensor import Tensor
|
||||
|
||||
# Test 1: Simple addition (no broadcasting)
|
||||
a = Tensor([1.0, 2.0, 3.0])
|
||||
b = Tensor([4.0, 5.0, 6.0])
|
||||
c = a + b
|
||||
|
||||
c.backward()
|
||||
|
||||
print("Test 1 - No broadcasting:")
|
||||
print(f"a.grad: {a.grad}") # Should be [1, 1, 1]
|
||||
print(f"b.grad: {b.grad}") # Should be [1, 1, 1]
|
||||
|
||||
# Test 2: Broadcasting
|
||||
a = Tensor([[1.0, 2.0]]) # shape (1, 2)
|
||||
b = Tensor([[3.0], [4.0]]) # shape (2, 1)
|
||||
c = a + b # shape (2, 2)
|
||||
|
||||
c.backward()
|
||||
|
||||
print("\nTest 2 - Broadcasting:")
|
||||
print(f"a.grad shape: {a.grad.shape}, values: {a.grad}") # Should be (1,2) with [[2, 2]]
|
||||
print(f"b.grad shape: {b.grad.shape}, values: {b.grad}") # Should be (2,1) with [[2], [2]]
|
||||
38
tests/test_linear.py
Normal file
38
tests/test_linear.py
Normal file
@@ -0,0 +1,38 @@
|
||||
from nanotorch.nn import Linear
|
||||
from nanotorch.tensor import Tensor
|
||||
|
||||
# Test 1: Single sample forward pass
|
||||
print("Test 1 - Single forward pass:")
|
||||
layer = Linear(3, 2) # 3 inputs -> 2 outputs
|
||||
|
||||
x = Tensor([1.0, 2.0, 3.0]) # shape (3,)
|
||||
y = layer(x) # shape (2,)
|
||||
|
||||
print(f"x.shape: {x.data.shape}")
|
||||
print(f"y.shape: {y.data.shape}")
|
||||
print(f"y.data: {y.data}")
|
||||
|
||||
# Test 2: Batch forward pass
|
||||
print("\nTest 2 - Batch forward pass:")
|
||||
x_batch = Tensor([[1.0, 2.0, 3.0],
|
||||
[4.0, 5.0, 6.0]]) # shape (2, 3) - batch of 2
|
||||
y_batch = layer(x_batch) # shape (2, 2)
|
||||
|
||||
print(f"x_batch.shape: {x_batch.data.shape}")
|
||||
print(f"y_batch.shape: {y_batch.data.shape}")
|
||||
|
||||
# Test 3: Backward pass
|
||||
print("\nTest 3 - Backward pass:")
|
||||
x = Tensor([[1.0, 2.0]]) # shape (1, 2)
|
||||
layer = Linear(2, 3) # 2 -> 3
|
||||
|
||||
y = layer(x)
|
||||
loss = y.sum() # Simple loss for testing
|
||||
|
||||
loss.backward()
|
||||
|
||||
print(f"W.grad shape: {layer.W.grad.shape}") # Should be (2, 3)
|
||||
print(f"b.grad shape: {layer.b.grad.shape}") # Should be (3,)
|
||||
print(f"x.grad shape: {x.grad.shape}") # Should be (1, 2)
|
||||
print(f"W.grad:\n{layer.W.grad}")
|
||||
print(f"b.grad: {layer.b.grad}")
|
||||
30
tests/test_matmul.py
Normal file
30
tests/test_matmul.py
Normal file
@@ -0,0 +1,30 @@
|
||||
from nanotorch.tensor import Tensor
|
||||
|
||||
# Test 1: Simple 2D matmul
|
||||
print("Test 1 - Simple 2D matmul:")
|
||||
x = Tensor([[1.0, 2.0, 3.0],
|
||||
[4.0, 5.0, 6.0]]) # (2, 3)
|
||||
y = Tensor([[7.0, 8.0],
|
||||
[9.0, 10.0],
|
||||
[11.0, 12.0]]) # (3, 2)
|
||||
z = x @ y # (2, 2)
|
||||
|
||||
z.backward()
|
||||
|
||||
print(f"z.data:\n{z.data}")
|
||||
print(f"x.grad:\n{x.grad}") # Should be z.grad @ y.T
|
||||
print(f"y.grad:\n{y.grad}") # Should be x.T @ z.grad
|
||||
|
||||
# Test 2: Vector-matrix multiplication
|
||||
print("\nTest 2 - Vector @ Matrix:")
|
||||
x = Tensor([1.0, 2.0, 3.0]) # (3,)
|
||||
y = Tensor([[4.0, 5.0],
|
||||
[6.0, 7.0],
|
||||
[8.0, 9.0]]) # (3, 2)
|
||||
z = x @ y # (2,)
|
||||
|
||||
z.backward()
|
||||
|
||||
print(f"z.data: {z.data}")
|
||||
print(f"x.grad: {x.grad}")
|
||||
print(f"y.grad:\n{y.grad}")
|
||||
45
tests/test_mlp.py
Normal file
45
tests/test_mlp.py
Normal file
@@ -0,0 +1,45 @@
|
||||
import numpy as np
|
||||
from nanotorch.tensor import Tensor
|
||||
from nanotorch.nn import MLP
|
||||
|
||||
# Test 1: MLP forward pass
|
||||
print("Test 1 - MLP forward pass:")
|
||||
model = MLP([2, 4, 3, 1]) # 2 inputs -> 4 hidden -> 3 hidden -> 1 output
|
||||
|
||||
x = Tensor([[1.0, 2.0]]) # Single sample, shape (1, 2)
|
||||
y = model(x) # shape (1, 1)
|
||||
|
||||
print(f"Input shape: {x.data.shape}")
|
||||
print(f"Output shape: {y.data.shape}")
|
||||
print(f"Output value: {y.data}")
|
||||
|
||||
# Test 2: Batch processing
|
||||
print("\nTest 2 - Batch processing:")
|
||||
x_batch = Tensor([[1.0, 2.0],
|
||||
[3.0, 4.0],
|
||||
[5.0, 6.0]]) # 3 samples, shape (3, 2)
|
||||
y_batch = model(x_batch) # shape (3, 1)
|
||||
|
||||
print(f"Batch input shape: {x_batch.data.shape}")
|
||||
print(f"Batch output shape: {y_batch.data.shape}")
|
||||
|
||||
# Test 3: Backward pass through entire network
|
||||
print("\nTest 3 - Full backward pass:")
|
||||
x = Tensor([[1.0, 2.0]])
|
||||
y_pred = model(x)
|
||||
y_true = Tensor([[5.0]])
|
||||
|
||||
# MSE loss
|
||||
loss = ((y_pred - y_true) ** 2).mean()
|
||||
|
||||
loss.backward()
|
||||
|
||||
print(f"Loss: {loss.data}")
|
||||
print(f"Number of parameters: {len(model.parameters())}")
|
||||
print(f"First layer W.grad shape: {model.layers[0].W.grad.shape}")
|
||||
print(f"Last layer W.grad shape: {model.layers[-1].W.grad.shape}")
|
||||
|
||||
# Verify gradients exist for all parameters
|
||||
all_have_grads = all(np.any(p.grad != 0) or p.grad.shape == ()
|
||||
for p in model.parameters())
|
||||
print(f"All parameters have gradients: {all_have_grads}")
|
||||
38
tests/test_mul.py
Normal file
38
tests/test_mul.py
Normal file
@@ -0,0 +1,38 @@
|
||||
from nanotorch.tensor import Tensor
|
||||
|
||||
# Test 1: Simple multiplication (no broadcasting)
|
||||
print("Test 1 - No broadcasting:")
|
||||
a = Tensor([2.0, 3.0, 4.0])
|
||||
b = Tensor([5.0, 6.0, 7.0])
|
||||
c = a * b # [10, 18, 28]
|
||||
|
||||
c.backward()
|
||||
|
||||
print(f"c.data: {c.data}")
|
||||
print(f"a.grad: {a.grad}") # Should be [5, 6, 7] (b's values)
|
||||
print(f"b.grad: {b.grad}") # Should be [2, 3, 4] (a's values)
|
||||
|
||||
# Test 2: Broadcasting case
|
||||
print("\nTest 2 - Broadcasting:")
|
||||
a = Tensor([[1.0, 2.0, 3.0]]) # shape (1, 3)
|
||||
b = Tensor([[2.0], [3.0]]) # shape (2, 1)
|
||||
c = a * b # shape (2, 3)
|
||||
|
||||
c.backward()
|
||||
|
||||
print(f"c.data:\n{c.data}")
|
||||
print(f"a.grad shape: {a.grad.shape}, values: {a.grad}") # Should be (1,3) with [[5, 5, 5]]
|
||||
print(f"b.grad shape: {b.grad.shape}, values: {b.grad}") # Should be (2,1) with [[6], [6]]
|
||||
|
||||
# Test 3: Chain rule test (addition + multiplication)
|
||||
print("\nTest 3 - Chain rule:")
|
||||
x = Tensor([2.0, 3.0])
|
||||
y = Tensor([4.0, 5.0])
|
||||
z = x * y # [8, 15]
|
||||
w = z + z # [16, 30]
|
||||
|
||||
w.backward()
|
||||
|
||||
print(f"w.data: {w.data}")
|
||||
print(f"x.grad: {x.grad}") # Should be [8, 10] (2 * y, because w = 2*x*y)
|
||||
print(f"y.grad: {y.grad}") # Should be [4, 6] (2 * x)
|
||||
58
tests/test_reductions.py
Normal file
58
tests/test_reductions.py
Normal file
@@ -0,0 +1,58 @@
|
||||
from nanotorch.tensor import Tensor
|
||||
|
||||
# Test 1: Sum all elements
|
||||
print("Test 1 - Sum (all elements):")
|
||||
x = Tensor([[1.0, 2.0, 3.0],
|
||||
[4.0, 5.0, 6.0]]) # shape (2, 3)
|
||||
y = x.sum() # 21.0, shape ()
|
||||
|
||||
y.backward()
|
||||
|
||||
print(f"y.data: {y.data}")
|
||||
print(f"x.grad:\n{x.grad}") # Should be all 1s
|
||||
|
||||
# Test 2: Sum along axis
|
||||
print("\nTest 2 - Sum (axis=1):")
|
||||
x = Tensor([[1.0, 2.0, 3.0],
|
||||
[4.0, 5.0, 6.0]]) # shape (2, 3)
|
||||
y = x.sum(axis=1) # [6, 15], shape (2,)
|
||||
|
||||
y.backward()
|
||||
|
||||
print(f"y.data: {y.data}")
|
||||
print(f"x.grad:\n{x.grad}") # Should be all 1s
|
||||
|
||||
# Test 3: Mean all elements
|
||||
print("\nTest 3 - Mean (all elements):")
|
||||
x = Tensor([[2.0, 4.0],
|
||||
[6.0, 8.0]]) # shape (2, 2)
|
||||
y = x.mean() # 5.0, shape ()
|
||||
|
||||
y.backward()
|
||||
|
||||
print(f"y.data: {y.data}")
|
||||
print(f"x.grad:\n{x.grad}") # Should be all 0.25 (1/4)
|
||||
|
||||
# Test 4: Mean along axis
|
||||
print("\nTest 4 - Mean (axis=0):")
|
||||
x = Tensor([[1.0, 2.0],
|
||||
[3.0, 4.0]]) # shape (2, 2)
|
||||
y = x.mean(axis=0) # [2, 3], shape (2,)
|
||||
|
||||
y.backward()
|
||||
|
||||
print(f"y.data: {y.data}")
|
||||
print(f"x.grad:\n{x.grad}") # Should be all 0.5 (1/2)
|
||||
|
||||
# Test 5: Chain rule with operations
|
||||
print("\nTest 5 - MSE Loss simulation:")
|
||||
pred = Tensor([1.0, 2.0, 3.0])
|
||||
target = Tensor([1.5, 2.5, 2.0])
|
||||
diff = pred - target
|
||||
squared = diff * diff
|
||||
loss = squared.mean()
|
||||
|
||||
loss.backward()
|
||||
|
||||
print(f"loss.data: {loss.data}")
|
||||
print(f"pred.grad: {pred.grad}") # Should show gradient for each prediction
|
||||
159
uv.lock
generated
159
uv.lock
generated
@@ -307,6 +307,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/93/0dd45cd283c32dea1545151d8c3637b4b8c53cdb3a625aeb2885b184d74d/fonttools-4.60.1-py3-none-any.whl", hash = "sha256:906306ac7afe2156fcf0042173d6ebbb05416af70f6b370967b47f8f00103bbb", size = 1143175, upload-time = "2025-09-29T21:13:24.134Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
version = "2.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ipykernel"
|
||||
version = "7.1.0"
|
||||
@@ -388,6 +397,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "joblib"
|
||||
version = "1.5.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e8/5d/447af5ea094b9e4c4054f82e223ada074c552335b9b4b2d14bd9b35a67c4/joblib-1.5.2.tar.gz", hash = "sha256:3faa5c39054b2f03ca547da9b2f52fde67c06240c31853f306aea97f13647b55", size = 331077, upload-time = "2025-08-27T12:15:46.575Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/e8/685f47e0d754320684db4425a0967f7d3fa70126bffd76110b7009a0090f/joblib-1.5.2-py3-none-any.whl", hash = "sha256:4e1f0bdbb987e6d843c70cf43714cb276623def372df3c22fe5266b2670bc241", size = 308396, upload-time = "2025-08-27T12:15:45.188Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jsonschema"
|
||||
version = "4.25.1"
|
||||
@@ -463,6 +481,13 @@ dependencies = [
|
||||
{ name = "matplotlib" },
|
||||
{ name = "mlx" },
|
||||
{ name = "nbconvert" },
|
||||
{ name = "numpy" },
|
||||
{ name = "scikit-learn" },
|
||||
]
|
||||
|
||||
[package.dev-dependencies]
|
||||
dev = [
|
||||
{ name = "pytest" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
@@ -472,8 +497,13 @@ requires-dist = [
|
||||
{ name = "matplotlib", specifier = ">=3.10.7" },
|
||||
{ name = "mlx", specifier = ">=0.29.4" },
|
||||
{ name = "nbconvert", specifier = ">=7.16.6" },
|
||||
{ name = "numpy", specifier = ">=2.3.4" },
|
||||
{ name = "scikit-learn", specifier = ">=1.7.2" },
|
||||
]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [{ name = "pytest", specifier = ">=9.0.1" }]
|
||||
|
||||
[[package]]
|
||||
name = "kiwisolver"
|
||||
version = "1.4.9"
|
||||
@@ -955,6 +985,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/73/cb/ac7874b3e5d58441674fb70742e6c374b28b0c7cb988d37d991cde47166c/platformdirs-4.5.0-py3-none-any.whl", hash = "sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3", size = 18651, upload-time = "2025-10-08T17:44:47.223Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pluggy"
|
||||
version = "1.6.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prompt-toolkit"
|
||||
version = "3.0.52"
|
||||
@@ -1038,6 +1077,22 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/10/5e/1aa9a93198c6b64513c9d7752de7422c06402de6600a8767da1524f9570b/pyparsing-3.2.5-py3-none-any.whl", hash = "sha256:e38a4f02064cf41fe6593d328d0512495ad1f3d8a91c4f73fc401b3079a59a5e", size = 113890, upload-time = "2025-09-21T04:11:04.117Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "9.0.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
{ name = "iniconfig" },
|
||||
{ name = "packaging" },
|
||||
{ name = "pluggy" },
|
||||
{ name = "pygments" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/07/56/f013048ac4bc4c1d9be45afd4ab209ea62822fb1598f40687e6bf45dcea4/pytest-9.0.1.tar.gz", hash = "sha256:3e9c069ea73583e255c3b21cf46b8d3c56f6e3a1a8f6da94ccb0fcf57b9d73c8", size = 1564125, upload-time = "2025-11-12T13:05:09.333Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/8b/6300fb80f858cda1c51ffa17075df5d846757081d11ab4aa35cef9e6258b/pytest-9.0.1-py3-none-any.whl", hash = "sha256:67be0030d194df2dfa7b556f2e56fb3c3315bd5c8822c6951162b92b32ce7dad", size = 373668, upload-time = "2025-11-12T13:05:07.379Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-dateutil"
|
||||
version = "2.9.0.post0"
|
||||
@@ -1188,6 +1243,101 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/7f/1a65ae870bc9d0576aebb0c501ea5dccf1ae2178fe2821042150ebd2e707/rpds_py-0.29.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2023473f444752f0f82a58dfcbee040d0a1b3d1b3c2ec40e884bd25db6d117d2", size = 225919, upload-time = "2025-11-16T14:50:14.734Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scikit-learn"
|
||||
version = "1.7.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "joblib" },
|
||||
{ name = "numpy" },
|
||||
{ name = "scipy" },
|
||||
{ name = "threadpoolctl" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/98/c2/a7855e41c9d285dfe86dc50b250978105dce513d6e459ea66a6aeb0e1e0c/scikit_learn-1.7.2.tar.gz", hash = "sha256:20e9e49ecd130598f1ca38a1d85090e1a600147b9c02fa6f15d69cb53d968fda", size = 7193136, upload-time = "2025-09-09T08:21:29.075Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/aa/3996e2196075689afb9fce0410ebdb4a09099d7964d061d7213700204409/scikit_learn-1.7.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8d91a97fa2b706943822398ab943cde71858a50245e31bc71dba62aab1d60a96", size = 9259818, upload-time = "2025-09-09T08:20:43.19Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/5d/779320063e88af9c4a7c2cf463ff11c21ac9c8bd730c4a294b0000b666c9/scikit_learn-1.7.2-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:acbc0f5fd2edd3432a22c69bed78e837c70cf896cd7993d71d51ba6708507476", size = 8636997, upload-time = "2025-09-09T08:20:45.468Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/d0/0c577d9325b05594fdd33aa970bf53fb673f051a45496842caee13cfd7fe/scikit_learn-1.7.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e5bf3d930aee75a65478df91ac1225ff89cd28e9ac7bd1196853a9229b6adb0b", size = 9478381, upload-time = "2025-09-09T08:20:47.982Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/70/8bf44b933837ba8494ca0fc9a9ab60f1c13b062ad0197f60a56e2fc4c43e/scikit_learn-1.7.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4d6e9deed1a47aca9fe2f267ab8e8fe82ee20b4526b2c0cd9e135cea10feb44", size = 9300296, upload-time = "2025-09-09T08:20:50.366Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/99/ed35197a158f1fdc2fe7c3680e9c70d0128f662e1fee4ed495f4b5e13db0/scikit_learn-1.7.2-cp312-cp312-win_amd64.whl", hash = "sha256:6088aa475f0785e01bcf8529f55280a3d7d298679f50c0bb70a2364a82d0b290", size = 8731256, upload-time = "2025-09-09T08:20:52.627Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/93/a3038cb0293037fd335f77f31fe053b89c72f17b1c8908c576c29d953e84/scikit_learn-1.7.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0b7dacaa05e5d76759fb071558a8b5130f4845166d88654a0f9bdf3eb57851b7", size = 9212382, upload-time = "2025-09-09T08:20:54.731Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/dd/9a88879b0c1104259136146e4742026b52df8540c39fec21a6383f8292c7/scikit_learn-1.7.2-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:abebbd61ad9e1deed54cca45caea8ad5f79e1b93173dece40bb8e0c658dbe6fe", size = 8592042, upload-time = "2025-09-09T08:20:57.313Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/af/c5e286471b7d10871b811b72ae794ac5fe2989c0a2df07f0ec723030f5f5/scikit_learn-1.7.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:502c18e39849c0ea1a5d681af1dbcf15f6cce601aebb657aabbfe84133c1907f", size = 9434180, upload-time = "2025-09-09T08:20:59.671Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/fd/df59faa53312d585023b2da27e866524ffb8faf87a68516c23896c718320/scikit_learn-1.7.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a4c328a71785382fe3fe676a9ecf2c86189249beff90bf85e22bdb7efaf9ae0", size = 9283660, upload-time = "2025-09-09T08:21:01.71Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/c7/03000262759d7b6f38c836ff9d512f438a70d8a8ddae68ee80de72dcfb63/scikit_learn-1.7.2-cp313-cp313-win_amd64.whl", hash = "sha256:63a9afd6f7b229aad94618c01c252ce9e6fa97918c5ca19c9a17a087d819440c", size = 8702057, upload-time = "2025-09-09T08:21:04.234Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/87/ef5eb1f267084532c8e4aef98a28b6ffe7425acbfd64b5e2f2e066bc29b3/scikit_learn-1.7.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:9acb6c5e867447b4e1390930e3944a005e2cb115922e693c08a323421a6966e8", size = 9558731, upload-time = "2025-09-09T08:21:06.381Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/f8/6c1e3fc14b10118068d7938878a9f3f4e6d7b74a8ddb1e5bed65159ccda8/scikit_learn-1.7.2-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:2a41e2a0ef45063e654152ec9d8bcfc39f7afce35b08902bfe290c2498a67a6a", size = 9038852, upload-time = "2025-09-09T08:21:08.628Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/87/066cafc896ee540c34becf95d30375fe5cbe93c3b75a0ee9aa852cd60021/scikit_learn-1.7.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98335fb98509b73385b3ab2bd0639b1f610541d3988ee675c670371d6a87aa7c", size = 9527094, upload-time = "2025-09-09T08:21:11.486Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/2b/4903e1ccafa1f6453b1ab78413938c8800633988c838aa0be386cbb33072/scikit_learn-1.7.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:191e5550980d45449126e23ed1d5e9e24b2c68329ee1f691a3987476e115e09c", size = 9367436, upload-time = "2025-09-09T08:21:13.602Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/aa/8444be3cfb10451617ff9d177b3c190288f4563e6c50ff02728be67ad094/scikit_learn-1.7.2-cp313-cp313t-win_amd64.whl", hash = "sha256:57dc4deb1d3762c75d685507fbd0bc17160144b2f2ba4ccea5dc285ab0d0e973", size = 9275749, upload-time = "2025-09-09T08:21:15.96Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/82/dee5acf66837852e8e68df6d8d3a6cb22d3df997b733b032f513d95205b7/scikit_learn-1.7.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fa8f63940e29c82d1e67a45d5297bdebbcb585f5a5a50c4914cc2e852ab77f33", size = 9208906, upload-time = "2025-09-09T08:21:18.557Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/30/9029e54e17b87cb7d50d51a5926429c683d5b4c1732f0507a6c3bed9bf65/scikit_learn-1.7.2-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:f95dc55b7902b91331fa4e5845dd5bde0580c9cd9612b1b2791b7e80c3d32615", size = 8627836, upload-time = "2025-09-09T08:21:20.695Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/18/4a52c635c71b536879f4b971c2cedf32c35ee78f48367885ed8025d1f7ee/scikit_learn-1.7.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9656e4a53e54578ad10a434dc1f993330568cfee176dff07112b8785fb413106", size = 9426236, upload-time = "2025-09-09T08:21:22.645Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/7e/290362f6ab582128c53445458a5befd471ed1ea37953d5bcf80604619250/scikit_learn-1.7.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96dc05a854add0e50d3f47a1ef21a10a595016da5b007c7d9cd9d0bffd1fcc61", size = 9312593, upload-time = "2025-09-09T08:21:24.65Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/87/24f541b6d62b1794939ae6422f8023703bbf6900378b2b34e0b4384dfefd/scikit_learn-1.7.2-cp314-cp314-win_amd64.whl", hash = "sha256:bb24510ed3f9f61476181e4db51ce801e2ba37541def12dc9333b946fc7a9cf8", size = 8820007, upload-time = "2025-09-09T08:21:26.713Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scipy"
|
||||
version = "1.16.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "numpy" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0a/ca/d8ace4f98322d01abcd52d381134344bf7b431eba7ed8b42bdea5a3c2ac9/scipy-1.16.3.tar.gz", hash = "sha256:01e87659402762f43bd2fee13370553a17ada367d42e7487800bf2916535aecb", size = 30597883, upload-time = "2025-10-28T17:38:54.068Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/40/41/5bf55c3f386b1643812f3a5674edf74b26184378ef0f3e7c7a09a7e2ca7f/scipy-1.16.3-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:81fc5827606858cf71446a5e98715ba0e11f0dbc83d71c7409d05486592a45d6", size = 36659043, upload-time = "2025-10-28T17:32:40.285Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/0f/65582071948cfc45d43e9870bf7ca5f0e0684e165d7c9ef4e50d783073eb/scipy-1.16.3-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:c97176013d404c7346bf57874eaac5187d969293bf40497140b0a2b2b7482e07", size = 28898986, upload-time = "2025-10-28T17:32:45.325Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/5e/36bf3f0ac298187d1ceadde9051177d6a4fe4d507e8f59067dc9dd39e650/scipy-1.16.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:2b71d93c8a9936046866acebc915e2af2e292b883ed6e2cbe5c34beb094b82d9", size = 20889814, upload-time = "2025-10-28T17:32:49.277Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/35/178d9d0c35394d5d5211bbff7ac4f2986c5488b59506fef9e1de13ea28d3/scipy-1.16.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:3d4a07a8e785d80289dfe66b7c27d8634a773020742ec7187b85ccc4b0e7b686", size = 23565795, upload-time = "2025-10-28T17:32:53.337Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/46/d1146ff536d034d02f83c8afc3c4bab2eddb634624d6529a8512f3afc9da/scipy-1.16.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0553371015692a898e1aa858fed67a3576c34edefa6b7ebdb4e9dde49ce5c203", size = 33349476, upload-time = "2025-10-28T17:32:58.353Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/2e/415119c9ab3e62249e18c2b082c07aff907a273741b3f8160414b0e9193c/scipy-1.16.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:72d1717fd3b5e6ec747327ce9bda32d5463f472c9dce9f54499e81fbd50245a1", size = 35676692, upload-time = "2025-10-28T17:33:03.88Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/27/82/df26e44da78bf8d2aeaf7566082260cfa15955a5a6e96e6a29935b64132f/scipy-1.16.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1fb2472e72e24d1530debe6ae078db70fb1605350c88a3d14bc401d6306dbffe", size = 36019345, upload-time = "2025-10-28T17:33:09.773Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/31/006cbb4b648ba379a95c87262c2855cd0d09453e500937f78b30f02fa1cd/scipy-1.16.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c5192722cffe15f9329a3948c4b1db789fbb1f05c97899187dcf009b283aea70", size = 38678975, upload-time = "2025-10-28T17:33:15.809Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/7f/acbd28c97e990b421af7d6d6cd416358c9c293fc958b8529e0bd5d2a2a19/scipy-1.16.3-cp312-cp312-win_amd64.whl", hash = "sha256:56edc65510d1331dae01ef9b658d428e33ed48b4f77b1d51caf479a0253f96dc", size = 38555926, upload-time = "2025-10-28T17:33:21.388Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/69/c5c7807fd007dad4f48e0a5f2153038dc96e8725d3345b9ee31b2b7bed46/scipy-1.16.3-cp312-cp312-win_arm64.whl", hash = "sha256:a8a26c78ef223d3e30920ef759e25625a0ecdd0d60e5a8818b7513c3e5384cf2", size = 25463014, upload-time = "2025-10-28T17:33:25.975Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/f1/57e8327ab1508272029e27eeef34f2302ffc156b69e7e233e906c2a5c379/scipy-1.16.3-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:d2ec56337675e61b312179a1ad124f5f570c00f920cc75e1000025451b88241c", size = 36617856, upload-time = "2025-10-28T17:33:31.375Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/13/7e63cfba8a7452eb756306aa2fd9b37a29a323b672b964b4fdeded9a3f21/scipy-1.16.3-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:16b8bc35a4cc24db80a0ec836a9286d0e31b2503cb2fd7ff7fb0e0374a97081d", size = 28874306, upload-time = "2025-10-28T17:33:36.516Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/65/3a9400efd0228a176e6ec3454b1fa998fbbb5a8defa1672c3f65706987db/scipy-1.16.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:5803c5fadd29de0cf27fa08ccbfe7a9e5d741bf63e4ab1085437266f12460ff9", size = 20865371, upload-time = "2025-10-28T17:33:42.094Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/d7/eda09adf009a9fb81827194d4dd02d2e4bc752cef16737cc4ef065234031/scipy-1.16.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:b81c27fc41954319a943d43b20e07c40bdcd3ff7cf013f4fb86286faefe546c4", size = 23524877, upload-time = "2025-10-28T17:33:48.483Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/6b/3f911e1ebc364cb81320223a3422aab7d26c9c7973109a9cd0f27c64c6c0/scipy-1.16.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0c3b4dd3d9b08dbce0f3440032c52e9e2ab9f96ade2d3943313dfe51a7056959", size = 33342103, upload-time = "2025-10-28T17:33:56.495Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/f6/4bfb5695d8941e5c570a04d9fcd0d36bce7511b7d78e6e75c8f9791f82d0/scipy-1.16.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7dc1360c06535ea6116a2220f760ae572db9f661aba2d88074fe30ec2aa1ff88", size = 35697297, upload-time = "2025-10-28T17:34:04.722Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/e1/6496dadbc80d8d896ff72511ecfe2316b50313bfc3ebf07a3f580f08bd8c/scipy-1.16.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:663b8d66a8748051c3ee9c96465fb417509315b99c71550fda2591d7dd634234", size = 36021756, upload-time = "2025-10-28T17:34:13.482Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/bd/a8c7799e0136b987bda3e1b23d155bcb31aec68a4a472554df5f0937eef7/scipy-1.16.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eab43fae33a0c39006a88096cd7b4f4ef545ea0447d250d5ac18202d40b6611d", size = 38696566, upload-time = "2025-10-28T17:34:22.384Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/01/1204382461fcbfeb05b6161b594f4007e78b6eba9b375382f79153172b4d/scipy-1.16.3-cp313-cp313-win_amd64.whl", hash = "sha256:062246acacbe9f8210de8e751b16fc37458213f124bef161a5a02c7a39284304", size = 38529877, upload-time = "2025-10-28T17:35:51.076Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/14/9d9fbcaa1260a94f4bb5b64ba9213ceb5d03cd88841fe9fd1ffd47a45b73/scipy-1.16.3-cp313-cp313-win_arm64.whl", hash = "sha256:50a3dbf286dbc7d84f176f9a1574c705f277cb6565069f88f60db9eafdbe3ee2", size = 25455366, upload-time = "2025-10-28T17:35:59.014Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/a3/9ec205bd49f42d45d77f1730dbad9ccf146244c1647605cf834b3a8c4f36/scipy-1.16.3-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:fb4b29f4cf8cc5a8d628bc8d8e26d12d7278cd1f219f22698a378c3d67db5e4b", size = 37027931, upload-time = "2025-10-28T17:34:31.451Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/06/ca9fd1f3a4589cbd825b1447e5db3a8ebb969c1eaf22c8579bd286f51b6d/scipy-1.16.3-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:8d09d72dc92742988b0e7750bddb8060b0c7079606c0d24a8cc8e9c9c11f9079", size = 29400081, upload-time = "2025-10-28T17:34:39.087Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/56/933e68210d92657d93fb0e381683bc0e53a965048d7358ff5fbf9e6a1b17/scipy-1.16.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:03192a35e661470197556de24e7cb1330d84b35b94ead65c46ad6f16f6b28f2a", size = 21391244, upload-time = "2025-10-28T17:34:45.234Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/7e/779845db03dc1418e215726329674b40576879b91814568757ff0014ad65/scipy-1.16.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:57d01cb6f85e34f0946b33caa66e892aae072b64b034183f3d87c4025802a119", size = 23929753, upload-time = "2025-10-28T17:34:51.793Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/4b/f756cf8161d5365dcdef9e5f460ab226c068211030a175d2fc7f3f41ca64/scipy-1.16.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:96491a6a54e995f00a28a3c3badfff58fd093bf26cd5fb34a2188c8c756a3a2c", size = 33496912, upload-time = "2025-10-28T17:34:59.8Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/09/b5/222b1e49a58668f23839ca1542a6322bb095ab8d6590d4f71723869a6c2c/scipy-1.16.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cd13e354df9938598af2be05822c323e97132d5e6306b83a3b4ee6724c6e522e", size = 35802371, upload-time = "2025-10-28T17:35:08.173Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/8d/5964ef68bb31829bde27611f8c9deeac13764589fe74a75390242b64ca44/scipy-1.16.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:63d3cdacb8a824a295191a723ee5e4ea7768ca5ca5f2838532d9f2e2b3ce2135", size = 36190477, upload-time = "2025-10-28T17:35:16.7Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/f2/b31d75cb9b5fa4dd39a0a931ee9b33e7f6f36f23be5ef560bf72e0f92f32/scipy-1.16.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e7efa2681ea410b10dde31a52b18b0154d66f2485328830e45fdf183af5aefc6", size = 38796678, upload-time = "2025-10-28T17:35:26.354Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/1e/b3723d8ff64ab548c38d87055483714fefe6ee20e0189b62352b5e015bb1/scipy-1.16.3-cp313-cp313t-win_amd64.whl", hash = "sha256:2d1ae2cf0c350e7705168ff2429962a89ad90c2d49d1dd300686d8b2a5af22fc", size = 38640178, upload-time = "2025-10-28T17:35:35.304Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/f3/d854ff38789aca9b0cc23008d607ced9de4f7ab14fa1ca4329f86b3758ca/scipy-1.16.3-cp313-cp313t-win_arm64.whl", hash = "sha256:0c623a54f7b79dd88ef56da19bc2873afec9673a48f3b85b18e4d402bdd29a5a", size = 25803246, upload-time = "2025-10-28T17:35:42.155Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/f6/99b10fd70f2d864c1e29a28bbcaa0c6340f9d8518396542d9ea3b4aaae15/scipy-1.16.3-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:875555ce62743e1d54f06cdf22c1e0bc47b91130ac40fe5d783b6dfa114beeb6", size = 36606469, upload-time = "2025-10-28T17:36:08.741Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/74/043b54f2319f48ea940dd025779fa28ee360e6b95acb7cd188fad4391c6b/scipy-1.16.3-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:bb61878c18a470021fb515a843dc7a76961a8daceaaaa8bad1332f1bf4b54657", size = 28872043, upload-time = "2025-10-28T17:36:16.599Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/e1/24b7e50cc1c4ee6ffbcb1f27fe9f4c8b40e7911675f6d2d20955f41c6348/scipy-1.16.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:f2622206f5559784fa5c4b53a950c3c7c1cf3e84ca1b9c4b6c03f062f289ca26", size = 20862952, upload-time = "2025-10-28T17:36:22.966Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/3a/3e8c01a4d742b730df368e063787c6808597ccb38636ed821d10b39ca51b/scipy-1.16.3-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:7f68154688c515cdb541a31ef8eb66d8cd1050605be9dcd74199cbd22ac739bc", size = 23508512, upload-time = "2025-10-28T17:36:29.731Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/60/c45a12b98ad591536bfe5330cb3cfe1850d7570259303563b1721564d458/scipy-1.16.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8b3c820ddb80029fe9f43d61b81d8b488d3ef8ca010d15122b152db77dc94c22", size = 33413639, upload-time = "2025-10-28T17:36:37.982Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/bc/35957d88645476307e4839712642896689df442f3e53b0fa016ecf8a3357/scipy-1.16.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d3837938ae715fc0fe3c39c0202de3a8853aff22ca66781ddc2ade7554b7e2cc", size = 35704729, upload-time = "2025-10-28T17:36:46.547Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/15/89105e659041b1ca11c386e9995aefacd513a78493656e57789f9d9eab61/scipy-1.16.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:aadd23f98f9cb069b3bd64ddc900c4d277778242e961751f77a8cb5c4b946fb0", size = 36086251, upload-time = "2025-10-28T17:36:55.161Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/87/c0ea673ac9c6cc50b3da2196d860273bc7389aa69b64efa8493bdd25b093/scipy-1.16.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b7c5f1bda1354d6a19bc6af73a649f8285ca63ac6b52e64e658a5a11d4d69800", size = 38716681, upload-time = "2025-10-28T17:37:04.1Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/06/837893227b043fb9b0d13e4bd7586982d8136cb249ffb3492930dab905b8/scipy-1.16.3-cp314-cp314-win_amd64.whl", hash = "sha256:e5d42a9472e7579e473879a1990327830493a7047506d58d73fc429b84c1d49d", size = 39358423, upload-time = "2025-10-28T17:38:20.005Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/03/28bce0355e4d34a7c034727505a02d19548549e190bedd13a721e35380b7/scipy-1.16.3-cp314-cp314-win_arm64.whl", hash = "sha256:6020470b9d00245926f2d5bb93b119ca0340f0d564eb6fbaad843eaebf9d690f", size = 26135027, upload-time = "2025-10-28T17:38:24.966Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/6f/69f1e2b682efe9de8fe9f91040f0cd32f13cfccba690512ba4c582b0bc29/scipy-1.16.3-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:e1d27cbcb4602680a49d787d90664fa4974063ac9d4134813332a8c53dbe667c", size = 37028379, upload-time = "2025-10-28T17:37:14.061Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/2d/e826f31624a5ebbab1cd93d30fd74349914753076ed0593e1d56a98c4fb4/scipy-1.16.3-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:9b9c9c07b6d56a35777a1b4cc8966118fb16cfd8daf6743867d17d36cfad2d40", size = 29400052, upload-time = "2025-10-28T17:37:21.709Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/27/d24feb80155f41fd1f156bf144e7e049b4e2b9dd06261a242905e3bc7a03/scipy-1.16.3-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:3a4c460301fb2cffb7f88528f30b3127742cff583603aa7dc964a52c463b385d", size = 21391183, upload-time = "2025-10-28T17:37:29.559Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/d3/1b229e433074c5738a24277eca520a2319aac7465eea7310ea6ae0e98ae2/scipy-1.16.3-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:f667a4542cc8917af1db06366d3f78a5c8e83badd56409f94d1eac8d8d9133fa", size = 23930174, upload-time = "2025-10-28T17:37:36.306Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/9d/d9e148b0ec680c0f042581a2be79a28a7ab66c0c4946697f9e7553ead337/scipy-1.16.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f379b54b77a597aa7ee5e697df0d66903e41b9c85a6dd7946159e356319158e8", size = 33497852, upload-time = "2025-10-28T17:37:42.228Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/22/4e5f7561e4f98b7bea63cf3fd7934bff1e3182e9f1626b089a679914d5c8/scipy-1.16.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4aff59800a3b7f786b70bfd6ab551001cb553244988d7d6b8299cb1ea653b353", size = 35798595, upload-time = "2025-10-28T17:37:48.102Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/42/6644d714c179429fc7196857866f219fef25238319b650bb32dde7bf7a48/scipy-1.16.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:da7763f55885045036fabcebd80144b757d3db06ab0861415d1c3b7c69042146", size = 36186269, upload-time = "2025-10-28T17:37:53.72Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/70/64b4d7ca92f9cf2e6fc6aaa2eecf80bb9b6b985043a9583f32f8177ea122/scipy-1.16.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ffa6eea95283b2b8079b821dc11f50a17d0571c92b43e2b5b12764dc5f9b285d", size = 38802779, upload-time = "2025-10-28T17:37:59.393Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/61/82/8d0e39f62764cce5ffd5284131e109f07cf8955aef9ab8ed4e3aa5e30539/scipy-1.16.3-cp314-cp314t-win_amd64.whl", hash = "sha256:d9f48cafc7ce94cf9b15c6bffdc443a81a27bf7075cf2dcd5c8b40f85d10c4e7", size = 39471128, upload-time = "2025-10-28T17:38:05.259Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/47/a494741db7280eae6dc033510c319e34d42dd41b7ac0c7ead39354d1a2b5/scipy-1.16.3-cp314-cp314t-win_arm64.whl", hash = "sha256:21d9d6b197227a12dcbf9633320a4e34c6b0e51c57268df255a0942983bac562", size = 26464127, upload-time = "2025-10-28T17:38:11.34Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "six"
|
||||
version = "1.17.0"
|
||||
@@ -1220,6 +1370,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "threadpoolctl"
|
||||
version = "3.6.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b7/4d/08c89e34946fce2aec4fbb45c9016efd5f4d7f24af8e5d93296e935631d8/threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e", size = 21274, upload-time = "2025-03-13T13:49:23.031Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinycss2"
|
||||
version = "1.4.0"
|
||||
|
||||
Reference in New Issue
Block a user