concat and sigmoid function added, moved relu and sigmoid to _nn.py, renamed min and max to minimum and maximum like in numpy and updated functions to work with tensors

This commit is contained in:
Nicolas 2026-05-19 20:27:42 +02:00
parent d6ff9599f4
commit 4a71db0e38
7 changed files with 198 additions and 47 deletions

View File

@ -37,8 +37,9 @@ from ._target import Target, jit
from ._basic_types import NumLike, value, generic_sdb, iif
from ._vectors import vector, distance, scalar_projection, angle_between, rotate_vector, vector_projection
from ._quaternion import quaternion
from ._tensors import tensor, zeros, ones, arange, eye, identity, diagonal
from ._math import sqrt, abs, sign, sin, cos, tan, asin, acos, atan, atan2, log, exp, pow, get_42, clamp, min, max, relu
from ._tensors import tensor, zeros, ones, arange, eye, identity, diagonal, concat
from ._math import sqrt, abs, sign, sin, cos, tan, asin, acos, atan, atan2, log, exp, pow, get_42, clamp, minimum, maximum
from ._nn import relu, sigmoid
from ._autograd import grad
from ._tensors import tensor as matrix
from ._version import __version__ # Run "pip install -e ." to generate _version.py
@ -74,16 +75,18 @@ __all__ = [
"pow",
"get_42",
"clamp",
"min",
"max",
"minimum",
"maximum",
"relu",
"distance",
"scalar_projection",
"angle_between",
"rotate_vector",
"vector_projection",
"vector_projection",
"quaternion",
"grad",
"eye",
"concat",
"sigmoid",
"jit"
]

View File

@ -393,12 +393,20 @@ def clamp(x: U | value[U] | vector[U], min_value: U | value[U], max_value: U |
@overload
def min(x: value[U], y: U | value[U]) -> value[U]: ...
def minimum(x: U, y: U) -> U: ...
@overload
def min(x: U | value[U], y: value[U]) -> value[U]: ...
def minimum(x: value[U], y: U | value[U]) -> value[U]: ...
@overload
def min(x: U, y: U) -> U: ...
def min(x: U | value[U], y: U | value[U]) -> Any:
def minimum(x: U | value[U], y: value[U]) -> value[U]: ...
@overload
def minimum(x: vector[U], y: U | value[U] | vector[U]) -> vector[U]: ...
@overload
def minimum(x: U | value[U] | vector[U], y: vector[U]) -> vector[U]: ...
@overload
def minimum(x: tensor[U], y: U | value[U] | tensor[U]) -> tensor[U]: ...
@overload
def minimum(x: U | value[U] | tensor[U], y: tensor[U]) -> tensor[U]: ...
def minimum(x: U | value[U] | vector[U] | tensor[U], y: U | value[U] | vector[U] | tensor[U]) -> Any:
"""Minimum function to get the smaller of two values.
Arguments:
@ -408,22 +416,30 @@ def min(x: U | value[U], y: U | value[U]) -> Any:
Returns:
Minimum of x and y
"""
if isinstance(x, value):
if isinstance(x, tensor) or isinstance(y, tensor):
return _map2_tensor(x, y, minimum)
if isinstance(x, vector) or isinstance(y, vector):
return _map2_vector(x, y, minimum)
if isinstance(x, value) or isinstance(y, value):
return add_op('min', [x, y])
if isinstance(x, tensor):
return _map2_tensor(x, y, min)
if isinstance(x, vector):
return _map2_vector(x, y, min)
return x if x < y else y
@overload
def max(x: value[U], y: U | value[U]) -> value[U]: ...
def maximum(x: U, y: U) -> U: ...
@overload
def max(x: U | value[U], y: value[U]) -> value[U]: ...
def maximum(x: value[U], y: U | value[U]) -> value[U]: ...
@overload
def max(x: U, y: U) -> U: ...
def max(x: U | value[U], y: U | value[U]) -> Any:
def maximum(x: U | value[U], y: value[U]) -> value[U]: ...
@overload
def maximum(x: vector[U], y: U | value[U] | vector[U]) -> vector[U]: ...
@overload
def maximum(x: U | value[U] | vector[U], y: vector[U]) -> vector[U]: ...
@overload
def maximum(x: tensor[U], y: U | value[U] | tensor[U]) -> tensor[U]: ...
@overload
def maximum(x: U | value[U] | tensor[U], y: tensor[U]) -> tensor[U]: ...
def maximum(x: U | value[U] | vector[U] | tensor[U], y: U | value[U] | vector[U] | tensor[U]) -> Any:
"""Maximum function to get the larger of two values.
Arguments:
@ -433,12 +449,12 @@ def max(x: U | value[U], y: U | value[U]) -> Any:
Returns:
Maximum of x and y
"""
if isinstance(x, value):
if isinstance(x, tensor) or isinstance(y, tensor):
return _map2_tensor(x, y, maximum)
if isinstance(x, vector) or isinstance(y, vector):
return _map2_vector(x, y, maximum)
if isinstance(x, value) or isinstance(y, value):
return add_op('max', [x, y])
if isinstance(x, tensor):
return _map2_tensor(x, y, max)
if isinstance(x, vector):
return _map2_vector(x, y, max)
return x if x > y else y
@ -470,20 +486,6 @@ def lerp(v1: U | value[U] | vector[U], v2: U | value[U] | vector[U], t: unifloa
return v1 * (1 - t) + v2 * t
@overload
def relu(x: U) -> U: ...
@overload
def relu(x: value[U]) -> value[U]: ...
@overload
def relu(x: vector[U]) -> vector[U]: ...
@overload
def relu(x: tensor[U]) -> tensor[U]: ...
def relu(x: U | value[U] | vector[U] | tensor[U]) -> Any:
"""Returns x for x > 0 and otherwise 0."""
ret = x * (x > 0)
return ret
def _map2_vector(self: VecNumLike, other: VecNumLike, func: Callable[[Any, Any], value[U] | U]) -> vector[U]:
"""Applies a function to each element of the vector and a second vector or scalar."""
if isinstance(self, vector) and isinstance(other, vector):
@ -499,9 +501,9 @@ def _map2_vector(self: VecNumLike, other: VecNumLike, func: Callable[[Any, Any],
def _map2_tensor(self: TensorNumLike, other: TensorNumLike, func: Callable[[Any, Any], value[U] | U]) -> tensor[U]:
"""Applies a function to each element of the vector and a second vector or scalar."""
if isinstance(self, vector):
self = tensor(self.values, (len(self.values),))
self = tensor(self)
if isinstance(other, vector):
other = tensor(other.values, (len(other.values),))
other = tensor(other)
if isinstance(self, tensor) and isinstance(other, tensor):
assert self.shape == other.shape, "Tensors must have the same shape"
return tensor([func(x, y) for x, y in zip(self.values, other.values)], self.shape)

34
src/copapy/_nn.py Normal file
View File

@ -0,0 +1,34 @@
from . import vector
from . import tensor
from . import value
from typing import TypeVar, Any, overload
import copapy as cp
U = TypeVar("U", int, float)
@overload
def relu(x: U) -> U: ...
@overload
def relu(x: value[U]) -> value[U]: ...
@overload
def relu(x: vector[U]) -> vector[U]: ...
@overload
def relu(x: tensor[U]) -> tensor[U]: ...
def relu(x: U | value[U] | vector[U] | tensor[U]) -> Any:
"""Returns x for x > 0 and otherwise 0."""
ret = x * (x > 0)
return ret
@overload
def sigmoid(x: U) -> float: ...
@overload
def sigmoid(x: value[U]) -> value[float]: ...
@overload
def sigmoid(x: vector[U]) -> vector[float]: ...
@overload
def sigmoid(x: tensor[U]) -> tensor[float]: ...
def sigmoid(x: U | value[U] | vector[U] | tensor[U]) -> Any:
"""Sigmoid function to map any value to the range (0, 1)."""
return 1 / (1 + cp.exp(-x))

View File

@ -32,7 +32,7 @@ class tensor(ArrayType[TNum]):
self.shape: tuple[int, ...] = tuple(shape)
assert (isinstance(values, Sequence) and
any(isinstance(v, (value, int, float)) for v in values)), \
"Values must be a sequence of values if shape is provided"
"Values must be a sequence of scalars if shape is provided"
self.values: tuple[TNum | value[TNum], ...] = tuple(v for v in values if not isinstance(v, Sequence))
self.ndim: int = len(shape)
elif isinstance(values, (int, float)):
@ -856,6 +856,84 @@ def ones(shape: Sequence[int] | int) -> tensor[int]:
return tensor([1] * size, tuple(shape))
@overload
def concat(tensors: Sequence[vector[U]]) -> vector[U]: ...
@overload
def concat(tensors: Sequence[tensor[U]], axis: int = 0) -> tensor[U]: ...
def concat(tensors: Sequence[tensor[U] | vector[U]], axis: int = 0) -> tensor[U] | vector[U]:
"""Concatenate tensors or vectors along a specified axis.
Arguments:
tensors: Tensors or vectors to concatenate. Must have the same shape except for the specified axis.
axis: Axis along which to concatenate (default 0).
Returns:
A new tensor or vector resulting from concatenation.
"""
assert tensors, "At least one tensor must be provided"
# Check if all inputs are vectors
all_vectors = all(isinstance(item, vector) for item in tensors)
# Convert vectors to tensors for uniform processing
tensor_list: list[tensor[U]] = []
for item in tensors:
if isinstance(item, vector):
tensor_list.append(tensor(item.values, item.shape))
else:
tensor_list.append(item)
first_shape = tensor_list[0].shape
ndim = len(first_shape)
if axis < 0:
axis += ndim
assert 0 <= axis < ndim
# Shape checks
for t in tensor_list:
assert len(t.shape) == ndim
for i in range(ndim):
if i != axis:
assert t.shape[i] == first_shape[i]
# Output shape
new_shape = list(first_shape)
new_shape[axis] = sum(t.shape[axis] for t in tensor_list)
# Compute block sizes
inner_block: int = 1
for s in first_shape[axis + 1:]:
inner_block *= s
outer_block: int = 1
for s in first_shape[:axis]:
outer_block *= s
new_values: list[value[U] | U] = []
for outer in range(outer_block):
for t in tensor_list:
axis_size = t.shape[axis]
start = outer * axis_size * inner_block
end = start + axis_size * inner_block
new_values.extend(t.values[start:end])
result_tensor = tensor(new_values, tuple(new_shape))
# If all inputs were vectors and result is 1D, return as vector
if all_vectors and result_tensor.ndim == 1:
return vector(result_tensor.values)
return result_tensor
def flatten(t: tensor[U]) -> tensor[U]:
"""Flatten a tensor to a 1D tensor."""
return t.flatten()
def arange(start: int | float, stop: int | float | None = None,
step: int | float = 1) -> tensor[int] | tensor[float]:
"""Create a tensor with evenly spaced values.
@ -927,10 +1005,10 @@ def identity(size: int) -> tensor[int]:
@overload
def diagonal(vec: 'tensor[int] | vector[int]') -> tensor[int]: ...
def diagonal(vec: tensor[int] | vector[int]) -> tensor[int]: ...
@overload
def diagonal(vec: 'tensor[float] | vector[float]') -> tensor[float]: ...
def diagonal(vec: 'tensor[Any] | vector[Any]') -> 'tensor[Any]':
def diagonal(vec: tensor[float] | vector[float]) -> tensor[float]: ...
def diagonal(vec: tensor[Any] | vector[Any]) -> tensor[Any]:
"""Create a diagonal tensor from a 1D tensor.
Arguments:

View File

@ -23,8 +23,8 @@ def test_fine():
cp.abs(-c_f),
cp.sign(c_i),
cp.sign(-c_f),
cp.min(c_i, 5),
cp.max(c_f, 5))
cp.minimum(c_i, 5),
cp.maximum(c_f, 5))
re2_test = (a_f ** 2,
a_i ** -1,
@ -39,8 +39,8 @@ def test_fine():
cp.abs(-a_f),
cp.sign(a_i),
cp.sign(-a_f),
cp.min(a_i, 5),
cp.max(a_f, 5))
cp.minimum(a_i, 5),
cp.maximum(a_f, 5))
ret_refe = (a_f ** 2,
a_i ** -1,

View File

@ -3,6 +3,7 @@
import copapy as cp
def test_tensor_basic():
# Test 1: Create a scalar tensor
print("Test 1: Scalar tensor")
@ -225,6 +226,7 @@ def test_tensor_basic():
assert t.ndim == 3
print()
def test_tensor_slicing():
print("Test Numpy-style slicing")
t = cp.tensor([[10, 20, 30], [40, 50, 60], [70, 80, 90]])
@ -251,6 +253,25 @@ def test_tensor_slicing():
assert slice4[2] == 90
print()
def test_tensor_concat():
print("Test tensor concatenation")
t1 = cp.tensor([[1, 2], [3, 4]])
t2 = cp.tensor([[5, 6], [7, 8]])
t3 = cp.tensor([[9, 10], [11, 12]])
concat_0 = cp.concat([t1, t2, t3], axis=0)
print(f"Concatenate along axis 0: shape={concat_0.shape}")
assert concat_0.shape == (6, 2)
assert concat_0[4, 1] == 10
concat_1 = cp.concat([t1, t2, t3], axis=1)
print(f"Concatenate along axis 1: shape={concat_1.shape}")
assert concat_1.shape == (2, 6)
assert concat_1[1, 4] == 11
print()
if __name__ == "__main__":
test_tensor_basic()
print("All tests completed!")

View File

@ -114,6 +114,19 @@ def test_sort_vector():
assert ref == result
def test_vector_concat():
print("Test vector concatenation")
v1 = cp.vector([1, 2, 3])
v2 = cp.vector([4, 5])
v3 = cp.vector([6])
concat_vec = cp.concat([v1, v2, v3])
print(f"Concatenate vectors: shape={concat_vec.shape}")
assert concat_vec.shape == (6,)
assert concat_vec[4] == 5
print()
if __name__ == "__main__":
#test_vectors_init()
#test_compiled_vectors()