sharing the constant for scalar/vector and scalar/

matrix operations;
volatile property for net objects added
This commit is contained in:
Nicolas Kruse 2025-12-14 18:08:37 +01:00
parent a0ab604aad
commit 247fc1a28f
5 changed files with 132 additions and 41 deletions

View File

@ -61,7 +61,6 @@ class Node:
return hash(self.name) ^ hash(frozenset(a.source.node_hash for a in self.args))
return hash(self.name) ^ hash(tuple(a.source.node_hash for a in self.args))
def __hash__(self) -> int:
return self.node_hash
@ -77,6 +76,7 @@ class Net:
def __init__(self, dtype: str, source: Node):
self.dtype = dtype
self.source = source
self.volatile = False
def __repr__(self) -> str:
names = get_var_name(self)
@ -93,7 +93,7 @@ class value(Generic[TNum], Net):
Attributes:
dtype (str): Data type of this value.
"""
def __init__(self, source: TNum | Node, dtype: str | None = None):
def __init__(self, source: TNum | Node, dtype: str | None = None, volatile: bool = True):
"""Instance a value.
Args:
@ -113,6 +113,7 @@ class value(Generic[TNum], Net):
else:
self.source = CPConstant(source)
self.dtype = 'int'
self.volatile = volatile
@overload
def __add__(self: 'value[TNum]', other: 'value[TNum] | TNum') -> 'value[TNum]': ...
@ -220,33 +221,33 @@ class value(Generic[TNum], Net):
return add_op('floordiv', [other, self])
def __neg__(self: TCPNum) -> TCPNum:
if self.dtype == 'int':
return cast(TCPNum, add_op('sub', [value(0), self]))
return cast(TCPNum, add_op('sub', [value(0.0), self]))
if self.dtype == 'float':
return cast(TCPNum, add_op('sub', [value(0.0, volatile=False), self]))
return cast(TCPNum, add_op('sub', [value(0, volatile=False), self]))
def __gt__(self, other: TVarNumb) -> 'value[int]':
ret = add_op('gt', [self, other])
return value(ret.source, dtype='bool')
return value(ret.source, dtype='bool', volatile=False)
def __lt__(self, other: TVarNumb) -> 'value[int]':
ret = add_op('gt', [other, self])
return value(ret.source, dtype='bool')
return value(ret.source, dtype='bool', volatile=False)
def __ge__(self, other: TVarNumb) -> 'value[int]':
ret = add_op('ge', [self, other])
return value(ret.source, dtype='bool')
return value(ret.source, dtype='bool', volatile=False)
def __le__(self, other: TVarNumb) -> 'value[int]':
ret = add_op('ge', [other, self])
return value(ret.source, dtype='bool')
return value(ret.source, dtype='bool', volatile=False)
def __eq__(self, other: TVarNumb) -> 'value[int]': # type: ignore
ret = add_op('eq', [self, other], True)
return value(ret.source, dtype='bool')
return value(ret.source, dtype='bool', volatile=False)
def __ne__(self, other: TVarNumb) -> 'value[int]': # type: ignore
ret = add_op('ne', [self, other], True)
return value(ret.source, dtype='bool')
return value(ret.source, dtype='bool', volatile=False)
@overload
def __mod__(self: 'value[TNum]', other: 'value[TNum] | TNum') -> 'value[TNum]': ...
@ -358,7 +359,7 @@ class Op(Node):
def net_from_value(val: Any) -> value[Any]:
vi = CPConstant(val)
return value(vi, vi.dtype)
return value(vi, vi.dtype, False)
@overload

View File

@ -78,8 +78,14 @@ class matrix(Generic[TNum]):
tuple(a + b for a, b in zip(row1, row2))
for row1, row2 in zip(self.values, other.values)
)
if isinstance(other, value):
return matrix(
tuple(a + other for a in row)
for row in self.values
)
o = value(other, volatile=False) # Make sure a single constant is allocated
return matrix(
tuple(a + other for a in row)
tuple(a + o if isinstance(a, value) else a + other for a in row)
for row in self.values
)
@ -106,8 +112,14 @@ class matrix(Generic[TNum]):
tuple(a - b for a, b in zip(row1, row2))
for row1, row2 in zip(self.values, other.values)
)
if isinstance(other, value):
return matrix(
tuple(a - other for a in row)
for row in self.values
)
o = value(other, volatile=False) # Make sure a single constant is allocated
return matrix(
tuple(a - other for a in row)
tuple(a - o if isinstance(a, value) else a - other for a in row)
for row in self.values
)
@ -123,8 +135,14 @@ class matrix(Generic[TNum]):
tuple(b - a for a, b in zip(row1, row2))
for row1, row2 in zip(self.values, other.values)
)
if isinstance(other, value):
return matrix(
tuple(other - a for a in row)
for row in self.values
)
o = value(other, volatile=False) # Make sure a single constant is allocated
return matrix(
tuple(other - a for a in row)
tuple(o - a if isinstance(a, value) else other - a for a in row)
for row in self.values
)
@ -145,8 +163,14 @@ class matrix(Generic[TNum]):
tuple(a * b for a, b in zip(row1, row2))
for row1, row2 in zip(self.values, other.values)
)
if isinstance(other, value):
return matrix(
tuple(a * other for a in row)
for row in self.values
)
o = value(other, volatile=False) # Make sure a single constant is allocated
return matrix(
tuple(a * other for a in row)
tuple(a * o if isinstance(a, value) else a * other for a in row)
for row in self.values
)
@ -166,8 +190,14 @@ class matrix(Generic[TNum]):
tuple(a / b for a, b in zip(row1, row2))
for row1, row2 in zip(self.values, other.values)
)
if isinstance(other, value):
return matrix(
tuple(a / other for a in row)
for row in self.values
)
o = value(other, volatile=False) # Make sure a single constant is allocated
return matrix(
tuple(a / other for a in row)
tuple(a / o if isinstance(a, value) else a / other for a in row)
for row in self.values
)
@ -179,8 +209,14 @@ class matrix(Generic[TNum]):
tuple(b / a for a, b in zip(row1, row2))
for row1, row2 in zip(self.values, other.values)
)
if isinstance(other, value):
return matrix(
tuple(other / a for a in row)
for row in self.values
)
o = value(other, volatile=False) # Make sure a single constant is allocated
return matrix(
tuple(other / a for a in row)
tuple(o / a if isinstance(a, value) else other / a for a in row)
for row in self.values
)
@ -269,7 +305,7 @@ class matrix(Generic[TNum]):
"""Convert all elements to copapy values if any element is a copapy value."""
if any(isinstance(val, value) for row in self.values for val in row):
return matrix(
tuple(value(val) if not isinstance(val, value) else val for val in row)
tuple(value(val, volatile=False) if not isinstance(val, value) else val for val in row)
for row in self.values
)
else:

View File

@ -19,6 +19,6 @@ def mixed_sum(scalars: Iterable[int | float | value[Any]]) -> Any:
def mixed_homogenize(scalars: Iterable[T | value[T]]) -> Iterable[T] | Iterable[value[T]]:
if any(isinstance(val, value) for val in scalars):
return (value(val) if not isinstance(val, value) else val for val in scalars)
return (value(val, volatile=False) if not isinstance(val, value) else val for val in scalars)
else:
return (val for val in scalars if not isinstance(val, value))

View File

@ -1,6 +1,6 @@
from . import value
from ._mixed import mixed_sum, mixed_homogenize
from typing import TypeVar, Iterable, Any, overload, TypeAlias, Callable, Iterator, Generic
from typing import Sequence, TypeVar, Iterable, Any, overload, TypeAlias, Callable, Iterator, Generic
import copapy as cp
from ._helper_types import TNum
@ -57,7 +57,10 @@ class vector(Generic[TNum]):
if isinstance(other, vector):
assert len(self.values) == len(other.values)
return vector(a + b for a, b in zip(self.values, other.values))
return vector(a + other for a in self.values)
if isinstance(other, value):
return vector(a + other for a in self.values)
o = value(other, volatile=False) # Make sure a single constant is allocated
return vector(a + o if isinstance(a, value) else a + other for a in self.values)
@overload
def __radd__(self: 'vector[float]', other: VecNumLike) -> 'vector[float]': ...
@ -80,7 +83,10 @@ class vector(Generic[TNum]):
if isinstance(other, vector):
assert len(self.values) == len(other.values)
return vector(a - b for a, b in zip(self.values, other.values))
return vector(a - other for a in self.values)
if isinstance(other, value):
return vector(a - other for a in self.values)
o = value(other, volatile=False) # Make sure a single constant is allocated
return vector(a - o if isinstance(a, value) else a - other for a in self.values)
@overload
def __rsub__(self: 'vector[float]', other: VecNumLike) -> 'vector[float]': ...
@ -92,7 +98,10 @@ class vector(Generic[TNum]):
if isinstance(other, vector):
assert len(self.values) == len(other.values)
return vector(b - a for a, b in zip(self.values, other.values))
return vector(other - a for a in self.values)
if isinstance(other, value):
return vector(other - a for a in self.values)
o = value(other, volatile=False) # Make sure a single constant is allocated
return vector(o - a if isinstance(a, value) else other - a for a in self.values)
@overload
def __mul__(self: 'vector[int]', other: VecFloatLike) -> 'vector[float]': ...
@ -106,7 +115,10 @@ class vector(Generic[TNum]):
if isinstance(other, vector):
assert len(self.values) == len(other.values)
return vector(a * b for a, b in zip(self.values, other.values))
return vector(a * other for a in self.values)
if isinstance(other, value):
return vector(a * other for a in self.values)
o = value(other, volatile=False) # Make sure a single constant is allocated
return vector(a * o if isinstance(a, value) else a * other for a in self.values)
@overload
def __rmul__(self: 'vector[float]', other: VecNumLike) -> 'vector[float]': ...
@ -129,7 +141,10 @@ class vector(Generic[TNum]):
if isinstance(other, vector):
assert len(self.values) == len(other.values)
return vector(a ** b for a, b in zip(self.values, other.values))
return vector(a ** other for a in self.values)
if isinstance(other, value):
return vector(a ** other for a in self.values)
o = value(other, volatile=False) # Make sure a single constant is allocated
return vector(a ** o if isinstance(a, value) else a ** other for a in self.values)
@overload
def __rpow__(self: 'vector[float]', other: VecNumLike) -> 'vector[float]': ...
@ -138,19 +153,31 @@ class vector(Generic[TNum]):
@overload
def __rpow__(self, other: VecNumLike) -> 'vector[Any]': ...
def __rpow__(self, other: VecNumLike) -> Any:
return self ** other
if isinstance(other, vector):
assert len(self.values) == len(other.values)
return vector(b ** a for a, b in zip(self.values, other.values))
if isinstance(other, value):
return vector(other ** a for a in self.values)
o = value(other, volatile=False) # Make sure a single constant is allocated
return vector(o ** a if isinstance(a, value) else other ** a for a in self.values)
def __truediv__(self, other: VecNumLike) -> 'vector[float]':
if isinstance(other, vector):
assert len(self.values) == len(other.values)
return vector(a / b for a, b in zip(self.values, other.values))
return vector(a / other for a in self.values)
if isinstance(other, value):
return vector(a / other for a in self.values)
o = value(other, volatile=False) # Make sure a single constant is allocated
return vector(a / o if isinstance(a, value) else a / other for a in self.values)
def __rtruediv__(self, other: VecNumLike) -> 'vector[float]':
if isinstance(other, vector):
assert len(self.values) == len(other.values)
return vector(b / a for a, b in zip(self.values, other.values))
return vector(other / a for a in self.values)
if isinstance(other, value):
return vector(other / a for a in self.values)
o = value(other, volatile=False) # Make sure a single constant is allocated
return vector(o / a if isinstance(a, value) else other / a for a in self.values)
@overload
def dot(self: 'vector[int]', other: 'vector[int]') -> int | value[int]: ...
@ -191,37 +218,55 @@ class vector(Generic[TNum]):
if isinstance(other, vector):
assert len(self.values) == len(other.values)
return vector(a > b for a, b in zip(self.values, other.values))
return vector(a > other for a in self.values)
if isinstance(other, value):
return vector(a > other for a in self.values)
o = value(other, volatile=False) # Make sure a single constant is allocated
return vector(a > o if isinstance(a, value) else a > other for a in self.values)
def __lt__(self, other: VecNumLike) -> 'vector[int]':
if isinstance(other, vector):
assert len(self.values) == len(other.values)
return vector(a < b for a, b in zip(self.values, other.values))
return vector(a < other for a in self.values)
if isinstance(other, value):
return vector(a < other for a in self.values)
o = value(other, volatile=False) # Make sure a single constant is allocated
return vector(a < o if isinstance(a, value) else a < other for a in self.values)
def __ge__(self, other: VecNumLike) -> 'vector[int]':
if isinstance(other, vector):
assert len(self.values) == len(other.values)
return vector(a >= b for a, b in zip(self.values, other.values))
return vector(a >= other for a in self.values)
if isinstance(other, value):
return vector(a >= other for a in self.values)
o = value(other, volatile=False) # Make sure a single constant is allocated
return vector(a >= o if isinstance(a, value) else a >= other for a in self.values)
def __le__(self, other: VecNumLike) -> 'vector[int]':
if isinstance(other, vector):
assert len(self.values) == len(other.values)
return vector(a <= b for a, b in zip(self.values, other.values))
return vector(a <= other for a in self.values)
if isinstance(other, value):
return vector(a <= other for a in self.values)
o = value(other, volatile=False) # Make sure a single constant is allocated
return vector(a <= o if isinstance(a, value) else a <= other for a in self.values)
def __eq__(self, other: VecNumLike) -> 'vector[int]': # type: ignore
if isinstance(other, vector):
assert len(self.values) == len(other.values)
return vector(a == b for a, b in zip(self.values, other.values))
return vector(a == other for a in self.values)
def __eq__(self, other: VecNumLike | Sequence[int | float]) -> 'vector[int]': # type: ignore
if isinstance(other, vector | Sequence):
assert len(self) == len(other)
return vector(a == b for a, b in zip(self.values, other))
if isinstance(other, value):
return vector(a == other for a in self.values)
o = value(other, volatile=False) # Make sure a single constant is allocated
return vector(a == o if isinstance(a, value) else a == other for a in self.values)
def __ne__(self, other: VecNumLike) -> 'vector[int]': # type: ignore
if isinstance(other, vector):
assert len(self.values) == len(other.values)
return vector(a != b for a, b in zip(self.values, other.values))
return vector(a != other for a in self.values)
if isinstance(other, value):
return vector(a != other for a in self.values)
o = value(other, volatile=False) # Make sure a single constant is allocated
return vector(a != o if isinstance(a, value) else a != other for a in self.values)
@property
def shape(self) -> tuple[int]:
@ -256,6 +301,15 @@ class vector(Generic[TNum]):
"""Applies a function to each element of the vector and returns a new vector."""
return vector(func(x) for x in self.values)
def _map2(self, other: VecNumLike, func: Callable[[Any, Any], value[int | float]]) -> 'vector[Any]':
if isinstance(other, vector):
assert len(self.values) == len(other.values)
return vector(func(a, b) for a, b in zip(self.values, other.values))
if isinstance(other, value):
return vector(func(a, other) for a in self.values)
o = value(other, volatile=False) # Make sure a single constant is allocated
return vector(func(a, o) if isinstance(a, value) else a + other for a in self.values)
def cross_product(v1: vector[float], v2: vector[float]) -> vector[float]:
"""Calculate the cross product of two 3D vectors."""

View File

@ -103,8 +103,8 @@ def test_matrix_scalar_division():
m1 = cp.matrix([[6.0, 8.0], [12.0, 16.0]])
m2 = m1 / 2.0
assert m2[0] == pytest.approx((3.0, 4.0)) # pyright: ignore[reportUnknownMemberType]
assert m2[1] == pytest.approx((6.0, 8.0)) # pyright: ignore[reportUnknownMemberType]
assert list(m2[0]) == pytest.approx((3.0, 4.0)) # pyright: ignore[reportUnknownMemberType]
assert list(m2[1]) == pytest.approx((6.0, 8.0)) # pyright: ignore[reportUnknownMemberType]
def test_matrix_vector_multiplication():