From 44b215f728ef0caba1d79cedc2947be20e2d8973 Mon Sep 17 00:00:00 2001 From: Nicolas Kruse Date: Thu, 27 Nov 2025 10:10:13 +0100 Subject: [PATCH 01/39] check_for_qemu function in tests updated to prevent error in error if qemu is not available --- tests/test_compile_aarch64.py | 2 +- tests/test_compile_armv7.py | 2 +- tests/test_ops_aarch64.py | 2 +- tests/test_ops_armv7.py | 4 +++- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/test_compile_aarch64.py b/tests/test_compile_aarch64.py index 1f2dd33..552ab77 100644 --- a/tests/test_compile_aarch64.py +++ b/tests/test_compile_aarch64.py @@ -26,7 +26,7 @@ def run_command(command: list[str]) -> str: def check_for_qemu() -> bool: command = qemu_command + ['--version'] try: - result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='utf8', check=False) + result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False) except Exception: return False return result.returncode == 0 diff --git a/tests/test_compile_armv7.py b/tests/test_compile_armv7.py index bf0fa89..5b1c6bb 100644 --- a/tests/test_compile_armv7.py +++ b/tests/test_compile_armv7.py @@ -26,7 +26,7 @@ def run_command(command: list[str]) -> str: def check_for_qemu() -> bool: command = qemu_command + ['--version'] try: - result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='utf8', check=False) + result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False) except Exception: return False return result.returncode == 0 diff --git a/tests/test_ops_aarch64.py b/tests/test_ops_aarch64.py index 306454f..e912edf 100644 --- a/tests/test_ops_aarch64.py +++ b/tests/test_ops_aarch64.py @@ -41,7 +41,7 @@ def run_command(command: list[str]) -> str: def check_for_qemu() -> bool: command = qemu_command + ['--version'] try: - result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='utf8', check=False) + result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False) except: return False return result.returncode == 0 diff --git a/tests/test_ops_armv7.py b/tests/test_ops_armv7.py index 0fd1b2b..1b53ca9 100644 --- a/tests/test_ops_armv7.py +++ b/tests/test_ops_armv7.py @@ -17,6 +17,7 @@ if os.name == 'nt': else: qemu_command = ['qemu-arm'] + def parse_results(log_text: str) -> dict[int, bytes]: regex = r"^READ_DATA offs=(\d*) size=(\d*) data=(.*)$" matches = re.finditer(regex, log_text, re.MULTILINE) @@ -31,6 +32,7 @@ def parse_results(log_text: str) -> dict[int, bytes]: return var_dict + def run_command(command: list[str]) -> str: result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='utf8', check=False) assert result.returncode != 11, f"SIGSEGV (segmentation fault)\n -Error occurred: {result.stderr}\n -Output: {result.stdout}" @@ -41,7 +43,7 @@ def run_command(command: list[str]) -> str: def check_for_qemu() -> bool: command = qemu_command + ['--version'] try: - result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='utf8', check=False) + result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False) except: return False return result.returncode == 0 From 99a880861a8e04d017aa97af76b40637f7e31115 Mon Sep 17 00:00:00 2001 From: Nicolas Kruse Date: Thu, 27 Nov 2025 12:50:53 +0100 Subject: [PATCH 02/39] typing fixed, variable[bool] replaced by variable[int] --- src/copapy/_basic_types.py | 98 ++++++++++++++++++-------------------- src/copapy/_target.py | 18 ++----- src/copapy/_vectors.py | 2 +- 3 files changed, 51 insertions(+), 67 deletions(-) diff --git a/src/copapy/_basic_types.py b/src/copapy/_basic_types.py index 5782198..915dcef 100644 --- a/src/copapy/_basic_types.py +++ b/src/copapy/_basic_types.py @@ -3,15 +3,13 @@ from typing import Any, TypeVar, overload, TypeAlias, Generic, cast from ._stencils import stencil_database, detect_process_arch import copapy as cp -NumLike: TypeAlias = 'variable[int] | variable[float] | variable[bool] | int | float | bool' +NumLike: TypeAlias = 'variable[int] | variable[float] | int | float' unifloat: TypeAlias = 'variable[float] | float' uniint: TypeAlias = 'variable[int] | int' -unibool: TypeAlias = 'variable[bool] | bool' -uniboolint: TypeAlias = 'variable[bool] | bool | variable[int] | int' TCPNum = TypeVar("TCPNum", bound='variable[Any]') -TNum = TypeVar("TNum", int, float, bool) -TVarNumb: TypeAlias = 'variable[Any] | int | float | bool' +TNum = TypeVar("TNum", int, float) +TVarNumb: TypeAlias = 'variable[Any] | int | float' stencil_cache: dict[tuple[str, str], stencil_database] = {} @@ -107,20 +105,20 @@ class variable(Generic[TNum], Net): self.dtype = 'int' @overload - def __add__(self, other: TCPNum) -> TCPNum: ... - @overload - def __add__(self: TCPNum, other: uniint) -> TCPNum: ... + def __add__(self: 'variable[int]', other: uniint) -> 'variable[int]': ... @overload def __add__(self, other: unifloat) -> 'variable[float]': ... @overload - def __add__(self, other: NumLike) -> 'variable[float] | variable[int]': ... - def __add__(self, other: NumLike) -> Any: + def __add__(self: 'variable[float]', other: NumLike) -> 'variable[float]': ... + @overload + def __add__(self, other: TVarNumb) -> 'variable[float] | variable[int]': ... + def __add__(self, other: TVarNumb) -> Any: if isinstance(other, int | float) and other == 0: return self return add_op('add', [self, other], True) @overload - def __radd__(self: TCPNum, other: int) -> TCPNum: ... + def __radd__(self: 'variable[int]', other: int) -> 'variable[int]': ... @overload def __radd__(self, other: float) -> 'variable[float]': ... def __radd__(self, other: NumLike) -> Any: @@ -129,36 +127,36 @@ class variable(Generic[TNum], Net): return add_op('add', [self, other], True) @overload - def __sub__(self, other: TCPNum) -> TCPNum: ... - @overload - def __sub__(self: TCPNum, other: uniint) -> TCPNum: ... + def __sub__(self: 'variable[int]', other: uniint) -> 'variable[int]': ... @overload def __sub__(self, other: unifloat) -> 'variable[float]': ... @overload - def __sub__(self, other: NumLike) -> 'variable[float] | variable[int]': ... - def __sub__(self, other: NumLike) -> Any: + def __sub__(self: 'variable[float]', other: NumLike) -> 'variable[float]': ... + @overload + def __sub__(self, other: TVarNumb) -> 'variable[float] | variable[int]': ... + def __sub__(self, other: TVarNumb) -> Any: return add_op('sub', [self, other]) @overload - def __rsub__(self: TCPNum, other: int) -> TCPNum: ... + def __rsub__(self: 'variable[int]', other: int) -> 'variable[int]': ... @overload def __rsub__(self, other: float) -> 'variable[float]': ... def __rsub__(self, other: NumLike) -> Any: return add_op('sub', [other, self]) @overload - def __mul__(self, other: TCPNum) -> TCPNum: ... - @overload - def __mul__(self: TCPNum, other: uniint) -> TCPNum: ... + def __mul__(self: 'variable[int]', other: uniint) -> 'variable[int]': ... @overload def __mul__(self, other: unifloat) -> 'variable[float]': ... @overload - def __mul__(self, other: NumLike) -> 'variable[float] | variable[int]': ... - def __mul__(self, other: NumLike) -> Any: + def __mul__(self: 'variable[float]', other: NumLike) -> 'variable[float]': ... + @overload + def __mul__(self, other: TVarNumb) -> 'variable[float] | variable[int]': ... + def __mul__(self, other: TVarNumb) -> Any: return add_op('mul', [self, other], True) @overload - def __rmul__(self: TCPNum, other: int) -> TCPNum: ... + def __rmul__(self: 'variable[int]', other: int) -> 'variable[int]': ... @overload def __rmul__(self, other: float) -> 'variable[float]': ... def __rmul__(self, other: NumLike) -> Any: @@ -171,18 +169,18 @@ class variable(Generic[TNum], Net): return add_op('div', [other, self]) @overload - def __floordiv__(self, other: TCPNum) -> TCPNum: ... - @overload - def __floordiv__(self: TCPNum, other: uniint) -> TCPNum: ... + def __floordiv__(self: 'variable[int]', other: uniint) -> 'variable[int]': ... @overload def __floordiv__(self, other: unifloat) -> 'variable[float]': ... @overload - def __floordiv__(self, other: NumLike) -> 'variable[float] | variable[int]': ... - def __floordiv__(self, other: NumLike) -> Any: + def __floordiv__(self: 'variable[float]', other: NumLike) -> 'variable[float]': ... + @overload + def __floordiv__(self, other: TVarNumb) -> 'variable[float] | variable[int]': ... + def __floordiv__(self, other: TVarNumb) -> Any: return add_op('floordiv', [self, other]) @overload - def __rfloordiv__(self: TCPNum, other: int) -> TCPNum: ... + def __rfloordiv__(self: 'variable[int]', other: int) -> 'variable[int]': ... @overload def __rfloordiv__(self, other: float) -> 'variable[float]': ... def __rfloordiv__(self, other: NumLike) -> Any: @@ -191,27 +189,27 @@ class variable(Generic[TNum], Net): def __neg__(self: TCPNum) -> TCPNum: return cast(TCPNum, add_op('sub', [variable(0), self])) - def __gt__(self, other: TVarNumb) -> 'variable[bool]': + def __gt__(self, other: TVarNumb) -> 'variable[int]': ret = add_op('gt', [self, other]) return variable(ret.source, dtype='bool') - def __lt__(self, other: TVarNumb) -> 'variable[bool]': + def __lt__(self, other: TVarNumb) -> 'variable[int]': ret = add_op('gt', [other, self]) return variable(ret.source, dtype='bool') - def __ge__(self, other: TVarNumb) -> 'variable[bool]': + def __ge__(self, other: TVarNumb) -> 'variable[int]': ret = add_op('ge', [self, other]) return variable(ret.source, dtype='bool') - def __le__(self, other: TVarNumb) -> 'variable[bool]': + def __le__(self, other: TVarNumb) -> 'variable[int]': ret = add_op('ge', [other, self]) return variable(ret.source, dtype='bool') - def __eq__(self, other: TVarNumb) -> 'variable[bool]': # type: ignore + def __eq__(self, other: TVarNumb) -> 'variable[int]': # type: ignore ret = add_op('eq', [self, other], True) return variable(ret.source, dtype='bool') - def __ne__(self, other: TVarNumb) -> 'variable[bool]': # type: ignore + def __ne__(self, other: TVarNumb) -> 'variable[int]': # type: ignore ret = add_op('ne', [self, other], True) return variable(ret.source, dtype='bool') @@ -255,34 +253,34 @@ class variable(Generic[TNum], Net): return super().__hash__() # Bitwise and shift operations for cp[int] - def __lshift__(self, other: uniboolint) -> 'variable[int]': + def __lshift__(self, other: uniint) -> 'variable[int]': return add_op('lshift', [self, other]) - def __rlshift__(self, other: uniboolint) -> 'variable[int]': + def __rlshift__(self, other: uniint) -> 'variable[int]': return add_op('lshift', [other, self]) - def __rshift__(self, other: uniboolint) -> 'variable[int]': + def __rshift__(self, other: uniint) -> 'variable[int]': return add_op('rshift', [self, other]) - def __rrshift__(self, other: uniboolint) -> 'variable[int]': + def __rrshift__(self, other: uniint) -> 'variable[int]': return add_op('rshift', [other, self]) - def __and__(self, other: uniboolint) -> 'variable[int]': + def __and__(self, other: uniint) -> 'variable[int]': return add_op('bwand', [self, other], True) - def __rand__(self, other: uniboolint) -> 'variable[int]': + def __rand__(self, other: uniint) -> 'variable[int]': return add_op('rwand', [other, self], True) - def __or__(self, other: uniboolint) -> 'variable[int]': + def __or__(self, other: uniint) -> 'variable[int]': return add_op('bwor', [self, other], True) - def __ror__(self, other: uniboolint) -> 'variable[int]': + def __ror__(self, other: uniint) -> 'variable[int]': return add_op('bwor', [other, self], True) - def __xor__(self, other: uniboolint) -> 'variable[int]': + def __xor__(self, other: uniint) -> 'variable[int]': return add_op('bwxor', [self, other], True) - def __rxor__(self, other: uniboolint) -> 'variable[int]': + def __rxor__(self, other: uniint) -> 'variable[int]': return add_op('bwxor', [other, self], True) @@ -318,9 +316,7 @@ def net_from_value(value: Any) -> Net: @overload -def iif(expression: variable[Any], true_result: unibool, false_result: unibool) -> variable[bool]: ... # pyright: ignore[reportOverlappingOverload] -@overload -def iif(expression: variable[Any], true_result: uniint, false_result: uniint) -> variable[int]: ... +def iif(expression: variable[Any], true_result: uniint, false_result: uniint) -> variable[int]: ... # pyright: ignore[reportOverlappingOverload] @overload def iif(expression: variable[Any], true_result: unifloat, false_result: unifloat) -> variable[float]: ... @overload @@ -330,7 +326,7 @@ def iif(expression: float | int, true_result: TNum, false_result: variable[TNum] @overload def iif(expression: float | int, true_result: variable[TNum], false_result: TNum | variable[TNum]) -> variable[TNum]: ... def iif(expression: Any, true_result: Any, false_result: Any) -> Any: - allowed_type = (variable, int, float, bool) + allowed_type = (variable, int, float) assert isinstance(true_result, allowed_type) and isinstance(false_result, allowed_type), "Result type not supported" return (expression != 0) * true_result + (expression == 0) * false_result @@ -355,9 +351,7 @@ def add_op(op: str, args: list[variable[Any] | int | float], commutative: bool = def _get_data_and_dtype(value: Any) -> tuple[str, float | int]: - if isinstance(value, bool): - return ('bool', int(value)) - elif isinstance(value, int): + if isinstance(value, int): return ('int', int(value)) elif isinstance(value, float): return ('float', float(value)) diff --git a/src/copapy/_target.py b/src/copapy/_target.py index 75bc2a2..5e15912 100644 --- a/src/copapy/_target.py +++ b/src/copapy/_target.py @@ -28,7 +28,7 @@ class Target(): self.sdb = stencil_db_from_package(arch, optimization) self._variables: dict[Net, tuple[int, int, str]] = {} - def compile(self, *variables: int | float | variable[int] | variable[float] | variable[bool] | Iterable[int | float | variable[int] | variable[float] | variable[bool]]) -> None: + def compile(self, *variables: int | float | variable[int] | variable[float] | Iterable[int | float | variable[int] | variable[float]]) -> None: """Compiles the code to compute the given variables. Arguments: @@ -56,21 +56,11 @@ class Target(): assert coparun(dw.get_data()) > 0 @overload - def read_value(self, net: variable[bool]) -> bool: - ... - + def read_value(self, net: variable[float]) -> float: ... @overload - def read_value(self, net: variable[float]) -> float: - ... - + def read_value(self, net: variable[int]) -> int: ... @overload - def read_value(self, net: variable[int]) -> int: - ... - - @overload - def read_value(self, net: NumLike) -> float | int | bool: - ... - + def read_value(self, net: NumLike) -> float | int | bool: ... def read_value(self, net: NumLike) -> float | int | bool: """Reads the value of a variable. diff --git a/src/copapy/_vectors.py b/src/copapy/_vectors.py index 4664912..7542608 100644 --- a/src/copapy/_vectors.py +++ b/src/copapy/_vectors.py @@ -2,7 +2,7 @@ from . import variable from typing import Generic, TypeVar, Iterable, Any, overload, TypeAlias, Callable import copapy as cp -VecNumLike: TypeAlias = 'vector[int] | vector[float] | variable[int] | variable[float] | variable[bool] | int | float | bool' +VecNumLike: TypeAlias = 'vector[int] | vector[float] | variable[int] | variable[float] | int | float | bool' VecIntLike: TypeAlias = 'vector[int] | variable[int] | int' VecFloatLike: TypeAlias = 'vector[float] | variable[float] | float' T = TypeVar("T", int, float) From ad78c4089cefefa04d4e53ae4ad4f94ef973deb2 Mon Sep 17 00:00:00 2001 From: Nicolas Kruse Date: Thu, 27 Nov 2025 17:19:25 +0100 Subject: [PATCH 03/39] type annotations for iif function fixed --- src/copapy/_basic_types.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/copapy/_basic_types.py b/src/copapy/_basic_types.py index 915dcef..1d8ce67 100644 --- a/src/copapy/_basic_types.py +++ b/src/copapy/_basic_types.py @@ -322,9 +322,11 @@ def iif(expression: variable[Any], true_result: unifloat, false_result: unifloat @overload def iif(expression: float | int, true_result: TNum, false_result: TNum) -> TNum: ... @overload -def iif(expression: float | int, true_result: TNum, false_result: variable[TNum]) -> variable[TNum]: ... +def iif(expression: float | int, true_result: TNum | variable[TNum], false_result: variable[TNum]) -> variable[TNum]: ... @overload def iif(expression: float | int, true_result: variable[TNum], false_result: TNum | variable[TNum]) -> variable[TNum]: ... +@overload +def iif(expression: float | int | variable[Any], true_result: TNum | variable[TNum], false_result: TNum | variable[TNum]) -> variable[TNum] | TNum: ... def iif(expression: Any, true_result: Any, false_result: Any) -> Any: allowed_type = (variable, int, float) assert isinstance(true_result, allowed_type) and isinstance(false_result, allowed_type), "Result type not supported" From 89e8efb86479cfb65d912850bb1331a19bd7ccf1 Mon Sep 17 00:00:00 2001 From: Nicolas Kruse Date: Thu, 27 Nov 2025 17:20:07 +0100 Subject: [PATCH 04/39] read_value function extended to work with vectors --- src/copapy/_target.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/copapy/_target.py b/src/copapy/_target.py index 5e15912..6039ce5 100644 --- a/src/copapy/_target.py +++ b/src/copapy/_target.py @@ -1,4 +1,4 @@ -from typing import Iterable, overload +from typing import Iterable, overload, TypeVar, Any from . import _binwrite as binw from coparun_module import coparun, read_data_mem import struct @@ -6,6 +6,8 @@ from ._basic_types import stencil_db_from_package from ._basic_types import variable, Net, Node, Write, NumLike from ._compiler import compile_to_dag +T = TypeVar("T", int, float) + def add_read_command(dw: binw.data_writer, variables: dict[Net, tuple[int, int, str]], net: Net) -> None: assert net in variables, f"Variable {net} not found in data writer variables" @@ -41,6 +43,7 @@ class Target(): assert isinstance(net, Net), f"The folowing element is not a Net: {net}" nodes.append(Write(net)) else: + assert isinstance(s, Net), f"The folowing element is not a Net: {s}" nodes.append(Write(s)) dw, self._variables = compile_to_dag(nodes, self.sdb) @@ -56,12 +59,12 @@ class Target(): assert coparun(dw.get_data()) > 0 @overload - def read_value(self, net: variable[float]) -> float: ... - @overload - def read_value(self, net: variable[int]) -> int: ... + def read_value(self, net: variable[T]) -> T: ... @overload def read_value(self, net: NumLike) -> float | int | bool: ... - def read_value(self, net: NumLike) -> float | int | bool: + @overload + def read_value(self, net: Iterable[T | variable[T]]) -> list[T]: ... + def read_value(self, net: NumLike | variable[T] | Iterable[T | variable[T]]) -> Any: """Reads the value of a variable. Arguments: @@ -70,6 +73,13 @@ class Target(): Returns: Value of the variable """ + if isinstance(net, Iterable): + return [self.read_value(ni) if isinstance(ni, variable) else ni for ni in net] + + if isinstance(net, float | int): + print(f"Warning: value is not a copypy value") + return net + assert isinstance(net, Net), "Variable must be a copapy variable object" assert net in self._variables, f"Variable {net} not found. It might not have been compiled for the target." addr, lengths, var_type = self._variables[net] From 054ce6d50727e6ef5b96a8ba0cc98b41a4397398 Mon Sep 17 00:00:00 2001 From: Nicolas Kruse Date: Thu, 27 Nov 2025 17:20:46 +0100 Subject: [PATCH 05/39] __iter__ type annotation fixed for vector class --- src/copapy/_vectors.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/copapy/_vectors.py b/src/copapy/_vectors.py index 7542608..3d8a721 100644 --- a/src/copapy/_vectors.py +++ b/src/copapy/_vectors.py @@ -1,5 +1,5 @@ from . import variable -from typing import Generic, TypeVar, Iterable, Any, overload, TypeAlias, Callable +from typing import Generic, TypeVar, Iterable, Any, overload, TypeAlias, Callable, Iterator import copapy as cp VecNumLike: TypeAlias = 'vector[int] | vector[float] | variable[int] | variable[float] | int | float | bool' @@ -31,6 +31,12 @@ class vector(Generic[T]): def __getitem__(self, index: int) -> variable[T] | T: return self.values[index] + def __neg__(self) -> 'vector[float] | vector[int]': + return vector(-a for a in self.values) + + def __iter__(self) -> Iterator[variable[T] | T]: + return iter(self.values) + @overload def __add__(self: 'vector[int]', other: VecFloatLike) -> 'vector[float]': ... @overload @@ -163,11 +169,11 @@ class vector(Generic[T]): mag = self.magnitude() + epsilon return self / mag - def __neg__(self) -> 'vector[float] | vector[int]': - return vector(-a for a in self.values) - - def __iter__(self) -> Iterable[variable[T] | T]: - return iter(self.values) + def homogenize(self) -> 'vector[T]': + if any(isinstance(val, variable) for val in self.values): + return vector(variable(val) if not isinstance(val, variable) else val for val in self.values) + else: + return self def map(self, func: Callable[[Any], variable[U] | U]) -> 'vector[U]': """Applies a function to each element of the vector and returns a new vector.""" From a8eeea874b3ff300ba2c0cfbe631a0a08a065acd Mon Sep 17 00:00:00 2001 From: Nicolas Kruse Date: Thu, 27 Nov 2025 17:21:33 +0100 Subject: [PATCH 06/39] median, mean and argsort added for vector class --- src/copapy/filters.py | 64 +++++++++++++++++++++++++++++++++++++++++++ tests/test_vector.py | 29 +++++++++++++++----- 2 files changed, 86 insertions(+), 7 deletions(-) create mode 100644 src/copapy/filters.py diff --git a/src/copapy/filters.py b/src/copapy/filters.py new file mode 100644 index 0000000..8639b1f --- /dev/null +++ b/src/copapy/filters.py @@ -0,0 +1,64 @@ +from . import variable, vector +from ._basic_types import iif, unifloat, TNum +from typing import Any, Iterable + + +def homogenize_vector(input_values: Iterable[TNum | variable[TNum]]) -> Iterable[TNum] | Iterable[variable[TNum]]: + input_list = list(input_values) + if any(isinstance(val, variable) for val in input_list): + return (v if isinstance(v, variable) else variable(v) for v in input_list) + else: + return (v for v in input_list if not isinstance(v, variable)) + + +def _inv_argsort(input_vector: vector[TNum]) -> vector[int]: + positions = (sum((v1 > v2) for v2 in input_vector) for v1 in input_vector) + return vector(positions) + + +def argsort(input_vector: vector[TNum]) -> vector[int]: + """ + Perform an indirect sort. It returns an array of indices that index data + in sorted order. + + Args: + input_vector: The input vector containing numerical values. + + Returns: + Index array. + """ + return _inv_argsort(_inv_argsort(input_vector)) + + +def median(input_vector: vector[TNum]) -> TNum | variable[TNum]: + """ + Applies a median filter to the input vector and returns the median as a unifloat. + + Args: + input_vector: The input vector containing numerical values. + + Returns: + The median value of the input vector. + """ + vec = input_vector + ret = vec[0] + for v1 in vec: + n2 = len(vec) // 2 + 1 + lt = sum(v1 < v2 for v2 in vec) + gt = sum(v1 > v2 for v2 in vec) + ret = iif((lt < n2) & (gt < n2), v1, ret) + + return ret + + +def mean(input_vector: vector[Any]) -> unifloat: + """ + Applies a mean filter to the input vector and returns the mean as a unifloat. + + Args: + input_vector (vector): The input vector containing numerical values. + + Returns: + unifloat: The mean value of the input vector. + """ + return input_vector.sum() / len(input_vector) \ No newline at end of file diff --git a/tests/test_vector.py b/tests/test_vector.py index db44649..c893f6b 100644 --- a/tests/test_vector.py +++ b/tests/test_vector.py @@ -1,7 +1,7 @@ import math import copapy as cp import pytest - +from copapy import filters def test_vectors_init(): tt1 = cp.vector(range(3)) + cp.vector([1.1, 2.2, 3.3]) @@ -95,12 +95,27 @@ def test_non_compiled_vector_operations(): assert rotated.values[2] == pytest.approx(3.0, abs=1e-6) # pyright: ignore[reportUnknownMemberType] -if __name__ == "__main__": - test_vectors_init() - test_compiled_vectors() - test_vector_operations() - print('Finished!') +def test_sort_vector(): + vlist = [50, 21, 20, 10, 22, 1, 80, 70, 90] + t1 = cp.vector(cp.variable(v) for v in vlist) + #t1 = cp.vector(v for v in vlist) + + t2 = filters.median(t1) + + tg = cp.Target() + tg.compile(t2) + tg.run() + + result = tg.read_value(t2) + + ref = sorted(vlist)[len(vlist) // 2] + print(sorted(vlist)) + + assert ref == result + if __name__ == "__main__": - test_compiled_vectors() + #test_vectors_init() + #test_compiled_vectors() + test_sort_vector() print('Finished!') From bb4472eccbd3bcb76db362dcfb615b3c91ecc275 Mon Sep 17 00:00:00 2001 From: Nicolas Kruse Date: Mon, 1 Dec 2025 00:12:22 +0100 Subject: [PATCH 07/39] variable type hints updated --- src/copapy/_basic_types.py | 48 +++++++++++++++++++++++++++++--------- 1 file changed, 37 insertions(+), 11 deletions(-) diff --git a/src/copapy/_basic_types.py b/src/copapy/_basic_types.py index 1d8ce67..5170d04 100644 --- a/src/copapy/_basic_types.py +++ b/src/copapy/_basic_types.py @@ -104,6 +104,8 @@ class variable(Generic[TNum], Net): self.source = CPConstant(source) self.dtype = 'int' + @overload + def __add__(self: 'variable[TNum]', other: 'variable[TNum] | TNum') -> 'variable[TNum]': ... @overload def __add__(self: 'variable[int]', other: uniint) -> 'variable[int]': ... @overload @@ -117,6 +119,8 @@ class variable(Generic[TNum], Net): return self return add_op('add', [self, other], True) + @overload + def __radd__(self: 'variable[TNum]', other: TNum) -> 'variable[TNum]': ... @overload def __radd__(self: 'variable[int]', other: int) -> 'variable[int]': ... @overload @@ -126,6 +130,8 @@ class variable(Generic[TNum], Net): return self return add_op('add', [self, other], True) + @overload + def __sub__(self: 'variable[TNum]', other: 'variable[TNum] | TNum') -> 'variable[TNum]': ... @overload def __sub__(self: 'variable[int]', other: uniint) -> 'variable[int]': ... @overload @@ -137,6 +143,8 @@ class variable(Generic[TNum], Net): def __sub__(self, other: TVarNumb) -> Any: return add_op('sub', [self, other]) + @overload + def __rsub__(self: 'variable[TNum]', other: TNum) -> 'variable[TNum]': ... @overload def __rsub__(self: 'variable[int]', other: int) -> 'variable[int]': ... @overload @@ -144,6 +152,8 @@ class variable(Generic[TNum], Net): def __rsub__(self, other: NumLike) -> Any: return add_op('sub', [other, self]) + @overload + def __mul__(self: 'variable[TNum]', other: 'variable[TNum] | TNum') -> 'variable[TNum]': ... @overload def __mul__(self: 'variable[int]', other: uniint) -> 'variable[int]': ... @overload @@ -155,6 +165,8 @@ class variable(Generic[TNum], Net): def __mul__(self, other: TVarNumb) -> Any: return add_op('mul', [self, other], True) + @overload + def __rmul__(self: 'variable[TNum]', other: TNum) -> 'variable[TNum]': ... @overload def __rmul__(self: 'variable[int]', other: int) -> 'variable[int]': ... @overload @@ -168,6 +180,8 @@ class variable(Generic[TNum], Net): def __rtruediv__(self, other: NumLike) -> 'variable[float]': return add_op('div', [other, self]) + @overload + def __floordiv__(self: 'variable[TNum]', other: 'variable[TNum] | TNum') -> 'variable[TNum]': ... @overload def __floordiv__(self: 'variable[int]', other: uniint) -> 'variable[int]': ... @overload @@ -179,6 +193,8 @@ class variable(Generic[TNum], Net): def __floordiv__(self, other: TVarNumb) -> Any: return add_op('floordiv', [self, other]) + @overload + def __rfloordiv__(self: 'variable[TNum]', other: TNum) -> 'variable[TNum]': ... @overload def __rfloordiv__(self: 'variable[int]', other: int) -> 'variable[int]': ... @overload @@ -187,7 +203,9 @@ class variable(Generic[TNum], Net): return add_op('floordiv', [other, self]) def __neg__(self: TCPNum) -> TCPNum: - return cast(TCPNum, add_op('sub', [variable(0), self])) + if self.dtype == 'int': + return cast(TCPNum, add_op('sub', [variable(0), self])) + return cast(TCPNum, add_op('sub', [variable(0.0), self])) def __gt__(self, other: TVarNumb) -> 'variable[int]': ret = add_op('gt', [self, other]) @@ -214,36 +232,44 @@ class variable(Generic[TNum], Net): return variable(ret.source, dtype='bool') @overload - def __mod__(self, other: TCPNum) -> TCPNum: ... + def __mod__(self: 'variable[TNum]', other: 'variable[TNum] | TNum') -> 'variable[TNum]': ... @overload - def __mod__(self: TCPNum, other: uniint) -> TCPNum: ... + def __mod__(self: 'variable[int]', other: uniint) -> 'variable[int]': ... @overload def __mod__(self, other: unifloat) -> 'variable[float]': ... @overload - def __mod__(self, other: NumLike) -> 'variable[float] | variable[int]': ... - def __mod__(self, other: NumLike) -> Any: + def __mod__(self: 'variable[float]', other: NumLike) -> 'variable[float]': ... + @overload + def __mod__(self, other: TVarNumb) -> 'variable[float] | variable[int]': ... + def __mod__(self, other: TVarNumb) -> Any: return add_op('mod', [self, other]) @overload - def __rmod__(self: TCPNum, other: int) -> TCPNum: ... + def __rmod__(self: 'variable[TNum]', other: TNum) -> 'variable[TNum]': ... + @overload + def __rmod__(self: 'variable[int]', other: int) -> 'variable[int]': ... @overload def __rmod__(self, other: float) -> 'variable[float]': ... def __rmod__(self, other: NumLike) -> Any: return add_op('mod', [other, self]) @overload - def __pow__(self, other: TCPNum) -> TCPNum: ... + def __pow__(self: 'variable[TNum]', other: 'variable[TNum] | TNum') -> 'variable[TNum]': ... @overload - def __pow__(self: TCPNum, other: uniint) -> TCPNum: ... + def __pow__(self: 'variable[int]', other: uniint) -> 'variable[int]': ... @overload def __pow__(self, other: unifloat) -> 'variable[float]': ... @overload - def __pow__(self, other: NumLike) -> 'variable[float] | variable[int]': ... - def __pow__(self, other: NumLike) -> Any: + def __pow__(self: 'variable[float]', other: NumLike) -> 'variable[float]': ... + @overload + def __pow__(self, other: TVarNumb) -> 'variable[float] | variable[int]': ... + def __pow__(self, other: TVarNumb) -> Any: return cp.pow(self, other) @overload - def __rpow__(self: TCPNum, other: int) -> TCPNum: ... + def __rpow__(self: 'variable[TNum]', other: TNum) -> 'variable[TNum]': ... + @overload + def __rpow__(self: 'variable[int]', other: int) -> 'variable[int]': ... @overload def __rpow__(self, other: float) -> 'variable[float]': ... def __rpow__(self, other: NumLike) -> Any: From 29f5a262785c9cfdcd677faabe6c1eab4eda8d3e Mon Sep 17 00:00:00 2001 From: Nicolas Kruse Date: Mon, 1 Dec 2025 00:13:08 +0100 Subject: [PATCH 08/39] helping functions separated for vectors and matrices --- src/copapy/_mixed.py | 24 ++++++++++++++++++++++++ src/copapy/_vectors.py | 14 +++++++------- 2 files changed, 31 insertions(+), 7 deletions(-) create mode 100644 src/copapy/_mixed.py diff --git a/src/copapy/_mixed.py b/src/copapy/_mixed.py new file mode 100644 index 0000000..e4d2632 --- /dev/null +++ b/src/copapy/_mixed.py @@ -0,0 +1,24 @@ + +from . import variable +from typing import TypeVar, Iterable, Any, overload + +T = TypeVar("T", int, float) + + +@overload +def mixed_sum(scalars: Iterable[float | variable[float]]) -> float | variable[float]: ... +@overload +def mixed_sum(scalars: Iterable[int | variable[int]]) -> int | variable[int]: ... +@overload +def mixed_sum(scalars: Iterable[T | variable[T]]) -> T | variable[T]: ... +def mixed_sum(scalars: Iterable[int | float | variable[Any]]) -> Any: + sl = list(scalars) + return sum(a for a in sl if not isinstance(a, variable)) +\ + sum(a for a in sl if isinstance(a, variable)) + + +def mixed_homogenize(scalars: Iterable[T | variable[T]]) -> Iterable[T] | Iterable[variable[T]]: + if any(isinstance(val, variable) for val in scalars): + return (variable(val) if not isinstance(val, variable) else val for val in scalars) + else: + return (val for val in scalars if not isinstance(val, variable)) diff --git a/src/copapy/_vectors.py b/src/copapy/_vectors.py index 3d8a721..e6fbf40 100644 --- a/src/copapy/_vectors.py +++ b/src/copapy/_vectors.py @@ -1,4 +1,5 @@ from . import variable +from ._mixed import mixed_sum, mixed_homogenize from typing import Generic, TypeVar, Iterable, Any, overload, TypeAlias, Callable, Iterator import copapy as cp @@ -31,7 +32,7 @@ class vector(Generic[T]): def __getitem__(self, index: int) -> variable[T] | T: return self.values[index] - def __neg__(self) -> 'vector[float] | vector[int]': + def __neg__(self) -> 'vector[T]': return vector(-a for a in self.values) def __iter__(self) -> Iterator[variable[T] | T]: @@ -125,7 +126,7 @@ class vector(Generic[T]): def dot(self, other: 'vector[int] | vector[float]') -> float | int | variable[float] | variable[int]: ... def dot(self, other: 'vector[int] | vector[float]') -> Any: assert len(self.values) == len(other.values), "Vectors must be of same length." - return sum(a * b for a, b in zip(self.values, other.values)) + return mixed_sum(a * b for a, b in zip(self.values, other.values)) # @ operator @overload @@ -156,13 +157,12 @@ class vector(Generic[T]): def sum(self: 'vector[float]') -> float | variable[float]: ... def sum(self) -> Any: """Sum of all vector elements.""" - return sum(a for a in self.values if isinstance(a, variable)) +\ - sum(a for a in self.values if not isinstance(a, variable)) + return mixed_sum(self.values) def magnitude(self) -> 'float | variable[float]': """Magnitude (length) of the vector.""" - s = sum(a * a for a in self.values) - return cp.sqrt(s) if isinstance(s, variable) else cp.sqrt(s) + s = mixed_sum(a * a for a in self.values) + return cp.sqrt(s) def normalize(self) -> 'vector[float]': """Returns a normalized (unit length) version of the vector.""" @@ -171,7 +171,7 @@ class vector(Generic[T]): def homogenize(self) -> 'vector[T]': if any(isinstance(val, variable) for val in self.values): - return vector(variable(val) if not isinstance(val, variable) else val for val in self.values) + return vector(mixed_homogenize(self)) else: return self From e14092a2c3fd5c1e7adf176be399b2eca9d987a4 Mon Sep 17 00:00:00 2001 From: Nicolas Kruse Date: Mon, 1 Dec 2025 00:13:38 +0100 Subject: [PATCH 09/39] matrix class added --- src/copapy/__init__.py | 6 + src/copapy/_matrices.py | 291 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 297 insertions(+) create mode 100644 src/copapy/_matrices.py diff --git a/src/copapy/__init__.py b/src/copapy/__init__.py index 2670500..e3fd7d7 100644 --- a/src/copapy/__init__.py +++ b/src/copapy/__init__.py @@ -1,6 +1,7 @@ from ._target import Target from ._basic_types import NumLike, variable, generic_sdb, iif from ._vectors import vector, distance, scalar_projection, angle_between, rotate_vector, vector_projection +from ._matrices import matrix, identity, zeros, ones, diagonal from ._math import sqrt, abs, sin, cos, tan, asin, acos, atan, atan2, log, exp, pow, get_42, clamp, min, max __all__ = [ @@ -10,6 +11,11 @@ __all__ = [ "generic_sdb", "iif", "vector", + "matrix", + "identity", + "zeros", + "ones", + "diagonal", "sqrt", "abs", "sin", diff --git a/src/copapy/_matrices.py b/src/copapy/_matrices.py new file mode 100644 index 0000000..11ecc7d --- /dev/null +++ b/src/copapy/_matrices.py @@ -0,0 +1,291 @@ +from . import variable +from ._vectors import vector +from ._mixed import mixed_sum +from typing import Generic, TypeVar, Iterable, Any, overload, TypeAlias, Callable, Iterator + +MatNumLike: TypeAlias = 'matrix[int] | matrix[float] | variable[int] | variable[float] | int | float' +MatIntLike: TypeAlias = 'matrix[int] | variable[int] | int' +MatFloatLike: TypeAlias = 'matrix[float] | variable[float] | float' +TT = TypeVar("TT", int, float) +U = TypeVar("U", int, float) + + +class matrix(Generic[TT]): + """Mathematical matrix class supporting basic operations and interactions with variables. + """ + def __init__(self, values: Iterable[Iterable[TT | variable[TT]]]): + """Create a matrix with given values and variables. + + Args: + values: iterable of iterable of constant values and variables + """ + rows = tuple(tuple(row) for row in values) + if rows: + row_len = len(rows[0]) + assert all(len(row) == row_len for row in rows), "All rows must have the same length" + self.values: tuple[tuple[variable[TT] | TT, ...], ...] = tuple(rows) + self.rows = len(self.values) + self.cols = len(self.values[0]) if self.values else 0 + + def __repr__(self) -> str: + return f"matrix({self.values})" + + def __len__(self) -> int: + return self.rows + + def __getitem__(self, index: int) -> tuple[variable[TT] | TT, ...]: + return self.values[index] + + def __iter__(self) -> Iterator[tuple[variable[TT] | TT, ...]]: + return iter(self.values) + + def __neg__(self) -> 'matrix[TT]': + return matrix((-a for a in row) for row in self.values) + + @overload + def __add__(self: 'matrix[int]', other: MatFloatLike) -> 'matrix[float]': ... + @overload + def __add__(self: 'matrix[int]', other: MatIntLike) -> 'matrix[int]': ... + @overload + def __add__(self: 'matrix[float]', other: MatNumLike) -> 'matrix[float]': ... + @overload + def __add__(self, other: MatNumLike) -> 'matrix[int] | matrix[float]': ... + def __add__(self, other: MatNumLike) -> Any: + if isinstance(other, matrix): + assert self.rows == other.rows and self.cols == other.cols, \ + "Matrices must have the same dimensions" + return matrix( + tuple(a + b for a, b in zip(row1, row2)) + for row1, row2 in zip(self.values, other.values) + ) + return matrix( + tuple(a + other for a in row) + for row in self.values + ) + + @overload + def __radd__(self: 'matrix[float]', other: MatNumLike) -> 'matrix[float]': ... + @overload + def __radd__(self: 'matrix[int]', other: variable[int] | int) -> 'matrix[int]': ... + def __radd__(self, other: Any) -> Any: + return self + other + + @overload + def __sub__(self: 'matrix[int]', other: MatFloatLike) -> 'matrix[float]': ... + @overload + def __sub__(self: 'matrix[int]', other: MatIntLike) -> 'matrix[int]': ... + @overload + def __sub__(self: 'matrix[float]', other: MatNumLike) -> 'matrix[float]': ... + @overload + def __sub__(self, other: MatNumLike) -> 'matrix[int] | matrix[float]': ... + def __sub__(self, other: MatNumLike) -> Any: + if isinstance(other, matrix): + assert self.rows == other.rows and self.cols == other.cols, \ + "Matrices must have the same dimensions" + return matrix( + tuple(a - b for a, b in zip(row1, row2)) + for row1, row2 in zip(self.values, other.values) + ) + return matrix( + tuple(a - other for a in row) + for row in self.values + ) + + @overload + def __rsub__(self: 'matrix[float]', other: MatNumLike) -> 'matrix[float]': ... + @overload + def __rsub__(self: 'matrix[int]', other: variable[int] | int) -> 'matrix[int]': ... + def __rsub__(self, other: MatNumLike) -> Any: + if isinstance(other, matrix): + assert self.rows == other.rows and self.cols == other.cols, \ + "Matrices must have the same dimensions" + return matrix( + tuple(b - a for a, b in zip(row1, row2)) + for row1, row2 in zip(self.values, other.values) + ) + return matrix( + tuple(other - a for a in row) + for row in self.values + ) + + @overload + def __mul__(self: 'matrix[int]', other: MatFloatLike) -> 'matrix[float]': ... + @overload + def __mul__(self: 'matrix[int]', other: MatIntLike) -> 'matrix[int]': ... + @overload + def __mul__(self: 'matrix[float]', other: MatNumLike) -> 'matrix[float]': ... + @overload + def __mul__(self, other: MatNumLike) -> 'matrix[int] | matrix[float]': ... + def __mul__(self, other: MatNumLike) -> Any: + """Element-wise multiplication""" + if isinstance(other, matrix): + assert self.rows == other.rows and self.cols == other.cols, \ + "Matrices must have the same dimensions" + return matrix( + tuple(a * b for a, b in zip(row1, row2)) + for row1, row2 in zip(self.values, other.values) + ) + return matrix( + tuple(a * other for a in row) + for row in self.values + ) + + @overload + def __rmul__(self: 'matrix[float]', other: MatNumLike) -> 'matrix[float]': ... + @overload + def __rmul__(self: 'matrix[int]', other: variable[int] | int) -> 'matrix[int]': ... + def __rmul__(self, other: MatNumLike) -> Any: + return self * other + + def __truediv__(self, other: MatNumLike) -> 'matrix[float]': + """Element-wise division""" + if isinstance(other, matrix): + assert self.rows == other.rows and self.cols == other.cols, \ + "Matrices must have the same dimensions" + return matrix( + tuple(a / b for a, b in zip(row1, row2)) + for row1, row2 in zip(self.values, other.values) + ) + return matrix( + tuple(a / other for a in row) + for row in self.values + ) + + def __rtruediv__(self, other: MatNumLike) -> 'matrix[float]': + if isinstance(other, matrix): + assert self.rows == other.rows and self.cols == other.cols, \ + "Matrices must have the same dimensions" + return matrix( + tuple(b / a for a, b in zip(row1, row2)) + for row1, row2 in zip(self.values, other.values) + ) + return matrix( + tuple(other / a for a in row) + for row in self.values + ) + + @overload + def __matmul__(self: 'matrix[TT]', other: 'vector[TT]') -> 'vector[TT]': ... + @overload + def __matmul__(self: 'matrix[TT]', other: 'matrix[TT]') -> 'matrix[TT]': ... + def __matmul__(self: 'matrix[TT]', other: 'matrix[TT] | vector[TT]') -> 'matrix[TT] | vector[TT]': + """Matrix multiplication using @ operator""" + if isinstance(other, vector): + assert self.cols == len(other.values), \ + f"Matrix columns ({self.cols}) must match vector length ({len(other.values)})" + vec_result = (mixed_sum(a * b for a, b in zip(row, other.values)) for row in self.values) + return vector(vec_result) + else: + assert isinstance(other, matrix), "Cannot multiply matrix with {type(other)}" + assert self.cols == other.rows, \ + f"Matrix columns ({self.cols}) must match other matrix rows ({other.rows})" + result: list[list[TT | variable[TT]]] = [] + for row in self.values: + new_row: list[TT | variable[TT]] = [] + for col_idx in range(other.cols): + col = tuple(other.values[i][col_idx] for i in range(other.rows)) + element = sum(a * b for a, b in zip(row, col)) + new_row.append(element) + result.append(new_row) + return matrix(result) + + def transpose(self) -> 'matrix[TT]': + """Return the transpose of the matrix.""" + if not self.values: + return matrix([]) + return matrix( + tuple(self.values[i][j] for i in range(self.rows)) + for j in range(self.cols) + ) + + @property + def T(self): + return self.transpose() + + def row(self, index: int) -> vector[TT]: + """Get a row as a vector.""" + assert 0 <= index < self.rows, f"Row index {index} out of bounds" + return vector(self.values[index]) + + def col(self, index: int) -> vector[TT]: + """Get a column as a vector.""" + assert 0 <= index < self.cols, f"Column index {index} out of bounds" + return vector(self.values[i][index] for i in range(self.rows)) + + @overload + def trace(self: 'matrix[TT]') -> TT | variable[TT]: ... + @overload + def trace(self: 'matrix[int]') -> int | variable[int]: ... + @overload + def trace(self: 'matrix[float]') -> float | variable[float]: ... + def trace(self) -> Any: + """Calculate the trace (sum of diagonal elements).""" + assert self.rows == self.cols, "Trace is only defined for square matrices" + return mixed_sum(self.values[i][i] for i in range(self.rows)) + + @overload + def sum(self: 'matrix[TT]') -> TT | variable[TT]: ... + @overload + def sum(self: 'matrix[int]') -> int | variable[int]: ... + @overload + def sum(self: 'matrix[float]') -> float | variable[float]: ... + def sum(self) -> Any: + """Calculate the sum of all elements.""" + return mixed_sum(a for row in self.values for a in row) + + def map(self, func: Callable[[Any], variable[U] | U]) -> 'matrix[U]': + """Applies a function to each element of the matrix and returns a new matrix.""" + return matrix( + tuple(func(a) for a in row) + for row in self.values + ) + + def homogenize(self) -> 'matrix[TT]': + """Convert all elements to variables if any element is a variable.""" + if any(isinstance(val, variable) for row in self.values for val in row): + return matrix( + tuple(variable(val) if not isinstance(val, variable) else val for val in row) + for row in self.values + ) + else: + return self + + +# Utility functions for matrices + +def identity(size: int) -> matrix[int]: + """Create an identity matrix of given size.""" + return matrix( + tuple(1 if i == j else 0 for j in range(size)) + for i in range(size) + ) + + +def zeros(rows: int, cols: int) -> matrix[int]: + """Create a zero matrix of given dimensions.""" + return matrix( + tuple(0 for _ in range(cols)) + for _ in range(rows) + ) + + +def ones(rows: int, cols: int) -> matrix[int]: + """Create a matrix of ones with given dimensions.""" + return matrix( + tuple(1 for _ in range(cols)) + for _ in range(rows) + ) + + +@overload +def diagonal(vec: 'vector[int]') -> matrix[int]: ... +@overload +def diagonal(vec: 'vector[float]') -> matrix[float]: ... +def diagonal(vec: vector[Any]) -> matrix[Any]: + """Create a diagonal matrix from a vector.""" + size = len(vec) + + return matrix( + tuple(vec[i] if i == j else 0 for j in range(size)) + for i in range(size) + ) From a56320f6e0832776daf62261b8bc0093807cfb14 Mon Sep 17 00:00:00 2001 From: Nicolas Kruse Date: Mon, 1 Dec 2025 00:13:51 +0100 Subject: [PATCH 10/39] test for matrix class added --- tests/test_matrix.py | 273 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 273 insertions(+) create mode 100644 tests/test_matrix.py diff --git a/tests/test_matrix.py b/tests/test_matrix.py new file mode 100644 index 0000000..2930f34 --- /dev/null +++ b/tests/test_matrix.py @@ -0,0 +1,273 @@ +import copapy as cp +import pytest + + +def test_matrix_init(): + """Test basic matrix initialization""" + m1 = cp.matrix([[1, 2, 3], [4, 5, 6]]) + assert m1.rows == 2 + assert m1.cols == 3 + assert m1[0] == (1, 2, 3) + assert m1[1] == (4, 5, 6) + + +def test_matrix_with_variables(): + """Test matrix initialization with variables""" + m1 = cp.matrix([[cp.variable(1), 2], [3, cp.variable(4)]]) + assert m1.rows == 2 + assert m1.cols == 2 + assert isinstance(m1[0][0], cp.variable) + assert isinstance(m1[1][1], cp.variable) + + +def test_matrix_addition(): + """Test matrix addition""" + m1 = cp.matrix([[1, 2], [3, 4]]) + m2 = cp.matrix([[5, 6], [7, 8]]) + m3 = m1 + m2 + + assert m3[0] == (6, 8) + assert m3[1] == (10, 12) + + +def test_matrix_scalar_addition(): + """Test matrix addition with scalar""" + m1 = cp.matrix([[1, 2], [3, 4]]) + m2 = m1 + 5 + + assert m2[0] == (6, 7) + assert m2[1] == (8, 9) + + +def test_matrix_subtraction(): + """Test matrix subtraction""" + m1 = cp.matrix([[5, 6], [7, 8]]) + m2 = cp.matrix([[1, 2], [3, 4]]) + m3 = m1 - m2 + + assert m3[0] == (4, 4) + assert m3[1] == (4, 4) + + +def test_matrix_scalar_subtraction(): + """Test matrix subtraction with scalar""" + m1 = cp.matrix([[5, 6], [7, 8]]) + m2 = m1 - 2 + + assert m2[0] == (3, 4) + assert m2[1] == (5, 6) + + +def test_matrix_negation(): + """Test matrix negation""" + m1 = cp.matrix([[1, 2], [3, 4]]) + m2 = -m1 + + assert m2[0] == (-1, -2) + assert m2[1] == (-3, -4) + + +def test_matrix_element_wise_multiplication(): + """Test element-wise matrix multiplication""" + m1 = cp.matrix([[1, 2], [3, 4]]) + m2 = cp.matrix([[5, 6], [7, 8]]) + m3 = m1 * m2 + + assert m3[0] == (5, 12) + assert m3[1] == (21, 32) + + +def test_matrix_scalar_multiplication(): + """Test matrix multiplication with scalar""" + m1 = cp.matrix([[1, 2], [3, 4]]) + m2 = m1 * 3 + + assert m2[0] == (3, 6) + assert m2[1] == (9, 12) + + +def test_matrix_element_wise_division(): + """Test element-wise matrix division""" + m1 = cp.matrix([[6.0, 8.0], [12.0, 16.0]]) + m2 = cp.matrix([[2.0, 2.0], [3.0, 4.0]]) + m3 = m1 / m2 + + assert m3[0][0] == pytest.approx(3.0) # pyright: ignore[reportUnknownMemberType] + assert m3[0][1] == pytest.approx(4.0) # pyright: ignore[reportUnknownMemberType] + assert m3[1][0] == pytest.approx(4.0) # pyright: ignore[reportUnknownMemberType] + assert m3[1][1] == pytest.approx(4.0) # pyright: ignore[reportUnknownMemberType] + + +def test_matrix_scalar_division(): + """Test matrix division by scalar""" + 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] + + +def test_matrix_vector_multiplication(): + """Test matrix-vector multiplication using @ operator""" + m = cp.matrix([[1, 2, 3], [4, 5, 6]]) + v = cp.vector([7, 8, 9]) + result = m @ v + + assert isinstance(result, cp.vector) + assert len(result.values) == 2 + assert result.values[0] == 1*7 + 2*8 + 3*9 + assert result.values[1] == 4*7 + 5*8 + 6*9 + + +def test_matrix_matrix_multiplication(): + """Test matrix-matrix multiplication using @ operator""" + m1 = cp.matrix([[1, 2], [3, 4]]) + m2 = cp.matrix([[5, 6], [7, 8]]) + result = m1 @ m2 + + assert isinstance(result, cp.matrix) + assert result.rows == 2 + assert result.cols == 2 + assert result[0][0] == 1*5 + 2*7 + assert result[0][1] == 1*6 + 2*8 + assert result[1][0] == 3*5 + 4*7 + assert result[1][1] == 3*6 + 4*8 + + +def test_matrix_transpose(): + """Test matrix transpose""" + m = cp.matrix([[1, 2, 3], [4, 5, 6]]) + mt = m.transpose() + + assert mt.rows == 3 + assert mt.cols == 2 + assert mt[0] == (1, 4) + assert mt[1] == (2, 5) + assert mt[2] == (3, 6) + + +def test_matrix_transpose_property(): + """Test matrix transpose using .T property""" + m = cp.matrix([[1, 2, 3], [4, 5, 6]]) + mt = m.T + + assert mt.rows == 3 + assert mt.cols == 2 + assert mt[0] == (1, 4) + + +def test_matrix_row_access(): + """Test getting a row as a vector""" + m = cp.matrix([[1, 2, 3], [4, 5, 6]]) + row0 = m.row(0) + + assert isinstance(row0, cp.vector) + assert row0.values == (1, 2, 3) + + +def test_matrix_col_access(): + """Test getting a column as a vector""" + m = cp.matrix([[1, 2, 3], [4, 5, 6]]) + col1 = m.col(1) + + assert isinstance(col1, cp.vector) + assert col1.values == (2, 5) + + +def test_matrix_trace(): + """Test matrix trace (sum of diagonal elements)""" + m = cp.matrix([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) + trace = m.trace() + + assert trace == 1 + 5 + 9 + + +def test_matrix_sum(): + """Test sum of all matrix elements""" + m = cp.matrix([[1, 2, 3], [4, 5, 6]]) + total = m.sum() + + assert total == 1 + 2 + 3 + 4 + 5 + 6 + + +def test_matrix_map(): + """Test mapping a function over matrix elements""" + m = cp.matrix([[1, 2], [3, 4]]) + m_doubled = m.map(lambda x: x * 2) + + assert m_doubled[0] == (2, 4) + assert m_doubled[1] == (6, 8) + + +def test_matrix_homogenize(): + """Test homogenizing matrix (converting to all variables)""" + m = cp.matrix([[1, cp.variable(2)], [3, 4]]) + m_homo = m.homogenize() + + for row in m_homo: + for elem in row: + assert isinstance(elem, cp.variable) + + +def test_identity_matrix(): + """Test identity matrix creation""" + m = cp.identity(3) + + assert m.rows == 3 + assert m.cols == 3 + assert m[0] == (1, 0, 0) + assert m[1] == (0, 1, 0) + assert m[2] == (0, 0, 1) + + +def test_zeros_matrix(): + """Test zeros matrix creation""" + m = cp.zeros(2, 3) + + assert m.rows == 2 + assert m.cols == 3 + assert m[0] == (0, 0, 0) + assert m[1] == (0, 0, 0) + + +def test_ones_matrix(): + """Test ones matrix creation""" + m = cp.ones(2, 3) + + assert m.rows == 2 + assert m.cols == 3 + assert m[0] == (1, 1, 1) + assert m[1] == (1, 1, 1) + + +def test_diagonal_matrix(): + """Test diagonal matrix creation from vector""" + v = cp.vector([1, 2, 3]) + m = cp.diagonal(v) + + assert m.rows == 3 + assert m.cols == 3 + assert m[0] == (1, 0, 0) + assert m[1] == (0, 2, 0) + assert m[2] == (0, 0, 3) + + +def test_matrix_with_variables_compiled(): + """Test matrix operations with variables in compilation""" + m = cp.matrix([[cp.variable(1), 2], [3, cp.variable(4)]]) + v = cp.vector([cp.variable(5), 6]) + result = m @ v + + # result[0] = 1*5 + 2*6 = 17 + # result[1] = 3*5 + 4*6 = 39 + + tg = cp.Target() + tg.compile(result) + tg.run() + + assert tg.read_value(result.values[0]) == pytest.approx(17) # pyright: ignore[reportUnknownMemberType] + assert tg.read_value(result.values[1]) == pytest.approx(39) # pyright: ignore[reportUnknownMemberType] + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) \ No newline at end of file From 8fe51a2e45e0acb48ce9066b1f7e09ea0e9a9ad7 Mon Sep 17 00:00:00 2001 From: Nicolas Kruse Date: Mon, 1 Dec 2025 00:14:12 +0100 Subject: [PATCH 11/39] benchmark script added --- src/copapy/_matrices.py | 2 +- tests/benchmark.py | 61 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 tests/benchmark.py diff --git a/src/copapy/_matrices.py b/src/copapy/_matrices.py index 11ecc7d..92528c4 100644 --- a/src/copapy/_matrices.py +++ b/src/copapy/_matrices.py @@ -199,7 +199,7 @@ class matrix(Generic[TT]): ) @property - def T(self): + def T(self) -> 'matrix[TT]': return self.transpose() def row(self, index: int) -> vector[TT]: diff --git a/tests/benchmark.py b/tests/benchmark.py new file mode 100644 index 0000000..762e4c5 --- /dev/null +++ b/tests/benchmark.py @@ -0,0 +1,61 @@ +import copapy as cp +import numpy as np +import time + +def cp_vs_python(): + + from numpy.core._multiarray_umath import __cpu_features__ + print(__cpu_features__) + + for v_size in range(10, 800, 40): + + sum_size = 10 + #v_size = 400 + iter_size = 30000 + + v1 = cp.vector(cp.variable(float(v)) for v in range(v_size)) + v2 = cp.vector(cp.variable(float(v)) for v in [5]*v_size) + + v3 = sum((v1 + i) @ v2 for i in range(sum_size)) + + tg = cp.Target() + tg.compile(v3) + + time.sleep(0.1) + t0 = time.perf_counter() + for _ in range(iter_size): + tg.run() + elapsed_cp = time.perf_counter() - t0 + + #print(f"Copapy: {elapsed_cp:.4f} s") + + + v1 = cp.vector(float(v) for v in range(v_size)) + v2 = cp.vector(float(v) for v in [5]*v_size) + + time.sleep(0.1) + t0 = time.perf_counter() + for _ in range(iter_size//10): + v3 = sum((v1 + i) @ v2 for i in range(sum_size)) + + elapsed_python = time.perf_counter() - t0 + + #print(f"Python: {elapsed_python:.4f} s") + + + i = np.array(list(range(sum_size)),).reshape([sum_size, 1]) + + time.sleep(0.1) + t0 = time.perf_counter() + for _ in range(iter_size): + v3 = np.sum((v1 + i) @ v2) + + elapsed_np2 = time.perf_counter() - t0 + + #print(f"Numpy 2: {elapsed_np2:.4f} s") + + + print(f"{elapsed_cp}, {elapsed_python}, {elapsed_np2}") + +if __name__ == "__main__": + cp_vs_python() \ No newline at end of file From 9f77ef5642cd5d61e69ad215847c8886c84560b0 Mon Sep 17 00:00:00 2001 From: Nicolas Kruse Date: Mon, 1 Dec 2025 16:44:49 +0100 Subject: [PATCH 12/39] optimization for float/int operations added --- src/copapy/_basic_types.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/copapy/_basic_types.py b/src/copapy/_basic_types.py index 5170d04..99ee92c 100644 --- a/src/copapy/_basic_types.py +++ b/src/copapy/_basic_types.py @@ -163,6 +163,8 @@ class variable(Generic[TNum], Net): @overload def __mul__(self, other: TVarNumb) -> 'variable[float] | variable[int]': ... def __mul__(self, other: TVarNumb) -> Any: + if self.dtype == 'float' and isinstance(other, int): + other = float(other) # Prevent runtime conversion of consts; TODO: add this for other operations return add_op('mul', [self, other], True) @overload From 5bdd77db91a9bea2b49c970dd5b0be0c976a5153 Mon Sep 17 00:00:00 2001 From: Nicolas Kruse Date: Tue, 2 Dec 2025 16:51:20 +0100 Subject: [PATCH 13/39] readme updated --- README.md | 50 ++++++++++++++++++++++++++++++-------------------- 1 file changed, 30 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 1f49d4e..86e914b 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,31 @@ # Copapy +Copapy is a python framework for deterministic low latency realtime computations, targeting hardware applications - for example in the field of robotics, aerospace, embedded systems and control systems in general. -Copapy is a python based embedded domain specific language (eDSL) with copy & patch compiler. It uses the python interpreter for compilation. It generates a directed graph of variables and operations. The compiler generates machine code by composing pre-compiled stencils derived from compiled C code. +GPU frameworks like PyTorch, JAX and TensorFlow jump started the development in the field of AI. With the right balance of flexibility and performance they allow for fast iterations of new ideas while being performant enough to test them or even use them in production. -The Project targets applications that profit from fast implementation (e.g. prototyping) and require low latency realtime execution as well as minimizing risk of implementation errors not caught during compile time. This applies primarily to applications interfacing hardware, where runtime errors might lead to physical damage - for example in the field of robotics, aerospace, embedded systems and control systems in general. +This is exactly what Copapy is aiming for - but in the field of embedded realtime computation. While making use of the ergonomics of Python, the tooling and the general Python ecosystem, Copapy runs seamlessly optimized machine code. Despite being highly portable, the **copy and patch** compiler allows for effortless and fast deployment, without any dependencies beyond Python. It's designed to feel like writing python scripts, with a flat learning curve. But under the hood it produces high performance static typed and memory save code with a minimized set of possible runtime errors[^1]. To maximize productivity the framework provides detailed type hints to catch most errors even before compilation. -The language aims to be: +Embedded systems comes with a variety of CPU architectures. The **copy and patch** compiler already supports the most common ones [^3] and porting it to new architectures is effortless if a C compiler for the target architecture is available [^2]. The generated code depends only on the CPU architecture. The actual generated code does neither do system calls nor calling external libraries like libc. This allows Copapy for one to be highly deterministic and for the other it makes targeting different realtime operating systems or bare metal straight forward. + +The summarized main features are: - Fast to write & easy to read -- Type safe -- Having a predictable runtime -- No runtime errors +- Memory and type safety, minimal set of runtime errors [^1] +- deterministic execution +- Auto grad for efficient realtime optimizations +- Optimized machine code for the target architectures x68_64, Aarch64 and ARMv7 [^3] +- Very portable to new architectures [^2] +- Small python package, minimal dependencies, no cross compile toolchain required -Because the language is an embedded language, it can relay heavily on **python tooling**. While copapy is static typed, it uses Python to derive types during compile time wherever possible. It can get full benefit from python type checkers, to catch type errors even before compilation to improve ergonomics. +## Current state +While obviously hardware IO is a core aspect, this is not yet available. Therefore this package is at the moment a proof of concept with limited direct use. However the computation part is fully working and available for testing and playing with it by simply installing the package. At this point the project is quite close to being ready for integration into the first demonstration hardware platform. -## How it works -The **Compilation** step starts with tracing the python code to generate an acyclic directed graph (DAG) of variables and operations. The DAG can be optimized and gets than linearized to a sequence of operations. Each operation gets mapped to a pre-compiled stencil, which is a piece of machine code with placeholders for memory addresses. The compiler generates patch instructions to fill the placeholders with the correct memory addresses. The binary code build from the stencils, data for constants and the patch instructions are than passed to the Runner for execution. The runner allocates memory for the code and data, applies the patch instructions to correct memory addresses and finally executes the code. +Currently worked on: +- Array stencils for handling very large arrays and generate SIMD optimized code - e.g. for machine vision and neural network applications. +- For targeting Crossover‑MCUs, support for Thumb instructions required by ARM*-M is on the way. +- Constant-regrouping for symbolic optimization of the computation graph. -## Getting started -To install copapy, you can use pip: +## Getting started & example +To install copapy, you can use pip. Precompiled wheels are available for Linux (x86_64, Aarch64 and ARMv7), Windows (x86_64) and Mac OS (x86_64, Aarch64): ```bash pip install copapy @@ -47,10 +56,13 @@ print("Result d:", tg.read_value(d)) print("Result e:", tg.read_value(e)) ``` +## How it works +The **Compilation** step starts with tracing the python code to generate an acyclic directed graph (DAG) of variables and operations. The DAG can be optimized and gets than linearized to a sequence of operations. Each operation gets mapped to a pre-compiled stencil, which is a piece of machine code with placeholders for memory addresses. The compiler generates patch instructions to fill the placeholders with the correct memory addresses. The binary code build from the stencils, data for constants and the patch instructions are than passed to the runner for execution. The runner allocates memory for the code and data, applies the patch instructions to correct memory addresses and finally executes the code. + ## Developer Guide Contributions are welcome, please open an issue or submit a pull request on GitHub. -To get started with developing the package, first clone the repository to your local machine using Git: +To get started with developing the package, first clone the repository using Git: ```bash git clone https://github.com/Nonannet/copapy.git @@ -70,13 +82,13 @@ Build and install the package and dev dependencies: pip install -e .[dev] ``` -If the build fails because you have no suitable c compiler installed, you can either install a compiler or use the binary from pypi: +If the build fails because you have no suitable c compiler installed, you can either install a compiler (obviously) or use the binary from pypi: ```bash pip install copapy[dev] ``` -When running pytest it will use the binary from pypi but the local python code gets executed from the local repo. +When running pytest it will use the binary part from pypi but all the python code gets executed from the local repo. For running all tests you need the stencil object files and the compiled runner. You can download the stencils and binary runner from GitHub or build them with gcc yourself. @@ -92,12 +104,6 @@ To build the binaries from source on Linux run: bash tools/build.sh ``` -The runner (without the stencils) can be build on windows with: - -``` -tools\build -``` - Ensure that everything is set up correctly by running the tests: ```bash @@ -106,3 +112,7 @@ pytest ## License This project is licensed under GPL - see the [LICENSE](LICENSE) file for details. + +[^1]: Currently errors like divide by zero are possible. The feasibility of tacking value ranges in the type system is under investigation to be able to do checks at compile time. +[^2]: The compiler must support TCO (tail call optimization). Currently gcc as C compiler is supported. Porting to a new architecture requires to implement a subset of relocation types used by the architecture. +[^3]: Supported are x68_64, Aarch64, ARMv7 (non-Thumb); ARMv6/7-M (Thumb) is under development; code for x68 32 Bit is present but has open issues (low priority). \ No newline at end of file From d2df1dd3fb6e0128310c3eeb262091c7a7ff610e Mon Sep 17 00:00:00 2001 From: Nicolas Kruse Date: Tue, 2 Dec 2025 16:57:06 +0100 Subject: [PATCH 14/39] cp.sign and cp.relu added to _math.py --- src/copapy/__init__.py | 4 +++- src/copapy/_math.py | 34 +++++++++++++++++++++++++++++++++- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/src/copapy/__init__.py b/src/copapy/__init__.py index e3fd7d7..3948a7e 100644 --- a/src/copapy/__init__.py +++ b/src/copapy/__init__.py @@ -2,7 +2,7 @@ from ._target import Target from ._basic_types import NumLike, variable, generic_sdb, iif from ._vectors import vector, distance, scalar_projection, angle_between, rotate_vector, vector_projection from ._matrices import matrix, identity, zeros, ones, diagonal -from ._math import sqrt, abs, sin, cos, tan, asin, acos, atan, atan2, log, exp, pow, get_42, clamp, min, max +from ._math import sqrt, abs, sign, sin, cos, tan, asin, acos, atan, atan2, log, exp, pow, get_42, clamp, min, max, relu __all__ = [ "Target", @@ -19,6 +19,7 @@ __all__ = [ "sqrt", "abs", "sin", + "sign", "cos", "tan", "asin", @@ -32,6 +33,7 @@ __all__ = [ "clamp", "min", "max", + "relu", "distance", "scalar_projection", "angle_between", diff --git a/src/copapy/_math.py b/src/copapy/_math.py index 6e57f6a..6f775d5 100644 --- a/src/copapy/_math.py +++ b/src/copapy/_math.py @@ -278,6 +278,8 @@ def get_42(x: NumLike) -> variable[float] | float: return add_op('get_42', [x, x]) return float((int(x) * 3.0 + 42.0) * 5.0 + 21.0) + +#TODO: Add vector support @overload def abs(x: U) -> U: ... @overload @@ -292,7 +294,26 @@ def abs(x: U | variable[U]) -> Any: Absolute value of x """ ret = (x < 0) * -x + (x >= 0) * x - return ret # pyright: ignore[reportReturnType] + return ret # REMpyright: ignore[reportReturnType] + + +#TODO: Add vector support +@overload +def sign(x: U) -> U: ... +@overload +def sign(x: variable[U]) -> variable[U]: ... +def sign(x: U | variable[U]) -> Any: + """Return 1 for positive numbers and -1 for negative numbers. + For an input of 0 the return value is 0. + + Arguments: + x: Input value + + Returns: + -1, 0 or 1 + """ + ret = (x > 0) - (x < 0) + return ret @overload @@ -381,6 +402,17 @@ def lerp(v1: U | variable[U] | vector[U], v2: U | variable[U] | vector[U], t: U return v1 * (1 - t) + v2 * t +#TODO: Add vector support +@overload +def relu(x: U) -> U: ... +@overload +def relu(x: variable[U]) -> variable[U]: ... +def relu(x: U | variable[U]) -> Any: + """Returns x for x > 0 and otherwise 0.""" + ret = (x > 0) * x + return ret + + def _map2(self: VecNumLike, other: VecNumLike, func: Callable[[Any, Any], variable[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): From cc5582ae737d414743195ca3fe46cb7439a141a0 Mon Sep 17 00:00:00 2001 From: Nicolas Kruse Date: Tue, 2 Dec 2025 16:59:14 +0100 Subject: [PATCH 15/39] replaced list type by Sequence to improve type hinting --- src/copapy/_basic_types.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/copapy/_basic_types.py b/src/copapy/_basic_types.py index 99ee92c..e3ef3e5 100644 --- a/src/copapy/_basic_types.py +++ b/src/copapy/_basic_types.py @@ -1,5 +1,5 @@ import pkgutil -from typing import Any, TypeVar, overload, TypeAlias, Generic, cast +from typing import Any, Sequence, TypeVar, overload, TypeAlias, Generic, cast from ._stencils import stencil_database, detect_process_arch import copapy as cp @@ -49,7 +49,7 @@ class Node: name (str): The name of the operation this Node represents. """ def __init__(self) -> None: - self.args: list[Net] = [] + self.args: Sequence[Net] = [] self.name: str = '' def __repr__(self) -> str: @@ -67,6 +67,7 @@ class Net: def __init__(self, dtype: str, source: Node): self.dtype = dtype self.source = source + self.grad: NumLike = 1 def __repr__(self) -> str: names = get_var_name(self) @@ -103,6 +104,8 @@ class variable(Generic[TNum], Net): else: self.source = CPConstant(source) self.dtype = 'int' + + self.grad = 1 @overload def __add__(self: 'variable[TNum]', other: 'variable[TNum] | TNum') -> 'variable[TNum]': ... @@ -332,15 +335,15 @@ class Write(Node): class Op(Node): - def __init__(self, typed_op_name: str, args: list[Net]): + def __init__(self, typed_op_name: str, args: Sequence[Net]): assert not args or any(isinstance(t, Net) for t in args), 'args parameter must be of type list[Net]' self.name: str = typed_op_name - self.args: list[Net] = args + self.args: Sequence[Net] = args -def net_from_value(value: Any) -> Net: +def net_from_value(value: Any) -> variable[Any]: vi = CPConstant(value) - return Net(vi.dtype, vi) + return variable(vi, vi.dtype) @overload From a30ee12d0f6b6260f549f2d521032b3a49e5c3a1 Mon Sep 17 00:00:00 2001 From: Nicolas Kruse Date: Wed, 3 Dec 2025 17:27:29 +0100 Subject: [PATCH 16/39] fixed get_all_dag_edges prevent it emitting edges multiple time --- src/copapy/_compiler.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/copapy/_compiler.py b/src/copapy/_compiler.py index 4eade86..e4b1553 100644 --- a/src/copapy/_compiler.py +++ b/src/copapy/_compiler.py @@ -64,9 +64,15 @@ def get_all_dag_edges(nodes: Iterable[Node]) -> Generator[tuple[Node, Node], Non Yields: Tuples of (source_node, target_node) representing edges in the DAG """ + emitted_nodes: set[tuple[Node, Node]] = set() + for node in nodes: yield from get_all_dag_edges(net.source for net in node.args) - yield from ((net.source, node) for net in node.args) + for net in node.args: + edge = (net.source, node) + if edge not in emitted_nodes: + yield edge + emitted_nodes.add(edge) def get_const_nets(nodes: list[Node]) -> list[Net]: From c5048980c28a6462120cfbdc45ad0b26f62805af Mon Sep 17 00:00:00 2001 From: Nicolas Kruse Date: Wed, 3 Dec 2025 17:28:49 +0100 Subject: [PATCH 17/39] added optimization for + 0, - 0 and * 1 operations --- src/copapy/_basic_types.py | 49 +++++++++++++++++++++++++------------- 1 file changed, 32 insertions(+), 17 deletions(-) diff --git a/src/copapy/_basic_types.py b/src/copapy/_basic_types.py index e3ef3e5..b32828c 100644 --- a/src/copapy/_basic_types.py +++ b/src/copapy/_basic_types.py @@ -49,12 +49,22 @@ class Node: name (str): The name of the operation this Node represents. """ def __init__(self) -> None: - self.args: Sequence[Net] = [] + self.args: tuple[Net, ...] = tuple() self.name: str = '' + self.node_hash = 0 def __repr__(self) -> str: return f"Node:{self.name}({', '.join(str(a) for a in self.args) if self.args else (self.value if isinstance(self, CPConstant) else '')})" + def get_node_hash(self, commutative: bool = False) -> int: + if commutative: + 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 + class Net: """A Net represents a variable in the computation graph - or more generally it @@ -67,14 +77,13 @@ class Net: def __init__(self, dtype: str, source: Node): self.dtype = dtype self.source = source - self.grad: NumLike = 1 def __repr__(self) -> str: names = get_var_name(self) - return f"{'name:' + names[0] if names else 'id:' + str(id(self))[-5:]}" + return f"{'name:' + names[0] if names else 'id:' + str(hash(self))[-5:]}" def __hash__(self) -> int: - return id(self) + return self.source.node_hash class variable(Generic[TNum], Net): @@ -104,8 +113,6 @@ class variable(Generic[TNum], Net): else: self.source = CPConstant(source) self.dtype = 'int' - - self.grad = 1 @overload def __add__(self: 'variable[TNum]', other: 'variable[TNum] | TNum') -> 'variable[TNum]': ... @@ -118,7 +125,7 @@ class variable(Generic[TNum], Net): @overload def __add__(self, other: TVarNumb) -> 'variable[float] | variable[int]': ... def __add__(self, other: TVarNumb) -> Any: - if isinstance(other, int | float) and other == 0: + if not isinstance(other, variable) and other == 0: return self return add_op('add', [self, other], True) @@ -129,9 +136,7 @@ class variable(Generic[TNum], Net): @overload def __radd__(self, other: float) -> 'variable[float]': ... def __radd__(self, other: NumLike) -> Any: - if isinstance(other, int | float) and other == 0: - return self - return add_op('add', [self, other], True) + return self + other @overload def __sub__(self: 'variable[TNum]', other: 'variable[TNum] | TNum') -> 'variable[TNum]': ... @@ -144,6 +149,8 @@ class variable(Generic[TNum], Net): @overload def __sub__(self, other: TVarNumb) -> 'variable[float] | variable[int]': ... def __sub__(self, other: TVarNumb) -> Any: + if isinstance(other, int | float) and other == 0: + return self return add_op('sub', [self, other]) @overload @@ -168,6 +175,11 @@ class variable(Generic[TNum], Net): def __mul__(self, other: TVarNumb) -> Any: if self.dtype == 'float' and isinstance(other, int): other = float(other) # Prevent runtime conversion of consts; TODO: add this for other operations + if not isinstance(other, variable): + if other == 1: + return self + elif other == 0: + return 0 return add_op('mul', [self, other], True) @overload @@ -177,7 +189,7 @@ class variable(Generic[TNum], Net): @overload def __rmul__(self, other: float) -> 'variable[float]': ... def __rmul__(self, other: NumLike) -> Any: - return add_op('mul', [self, other], True) + return self * other def __truediv__(self, other: NumLike) -> 'variable[float]': return add_op('div', [self, other]) @@ -319,7 +331,8 @@ class CPConstant(Node): def __init__(self, value: int | float): self.dtype, self.value = _get_data_and_dtype(value) self.name = 'const_' + self.dtype - self.args = [] + self.args = tuple() + self.node_hash = id(self) class Write(Node): @@ -331,14 +344,16 @@ class Write(Node): net = Net(node.dtype, node) self.name = 'write_' + transl_type(net.dtype) - self.args = [net] + self.args = (net,) + self.node_hash = hash(self.name) ^ hash(net.source.node_hash) class Op(Node): - def __init__(self, typed_op_name: str, args: Sequence[Net]): + def __init__(self, typed_op_name: str, args: Sequence[Net], commutative: bool = False): assert not args or any(isinstance(t, Net) for t in args), 'args parameter must be of type list[Net]' self.name: str = typed_op_name - self.args: Sequence[Net] = args + self.args: tuple[Net, ...] = tuple(args) + self.node_hash = self.get_node_hash(commutative) def net_from_value(value: Any) -> variable[Any]: @@ -378,9 +393,9 @@ def add_op(op: str, args: list[variable[Any] | int | float], commutative: bool = result_type = generic_sdb.stencil_definitions[typed_op].split('_')[0] if result_type == 'float': - return variable[float](Op(typed_op, arg_nets), result_type) + return variable[float](Op(typed_op, arg_nets, commutative), result_type) else: - return variable[int](Op(typed_op, arg_nets), result_type) + return variable[int](Op(typed_op, arg_nets, commutative), result_type) def _get_data_and_dtype(value: Any) -> tuple[str, float | int]: From 61dc29e68b5bfd11253a3e3b8d3993c2134d39ea Mon Sep 17 00:00:00 2001 From: Nicolas Kruse Date: Wed, 3 Dec 2025 17:30:38 +0100 Subject: [PATCH 18/39] added auto grad feature "grad(..)" --- src/copapy/__init__.py | 2 + src/copapy/_autograd.py | 102 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+) create mode 100644 src/copapy/_autograd.py diff --git a/src/copapy/__init__.py b/src/copapy/__init__.py index 3948a7e..b8216c6 100644 --- a/src/copapy/__init__.py +++ b/src/copapy/__init__.py @@ -3,6 +3,7 @@ from ._basic_types import NumLike, variable, generic_sdb, iif from ._vectors import vector, distance, scalar_projection, angle_between, rotate_vector, vector_projection from ._matrices import matrix, identity, zeros, ones, diagonal from ._math import sqrt, abs, sign, sin, cos, tan, asin, acos, atan, atan2, log, exp, pow, get_42, clamp, min, max, relu +from ._autograd import grad __all__ = [ "Target", @@ -39,4 +40,5 @@ __all__ = [ "angle_between", "rotate_vector", "vector_projection", + "grad" ] diff --git a/src/copapy/_autograd.py b/src/copapy/_autograd.py new file mode 100644 index 0000000..613256d --- /dev/null +++ b/src/copapy/_autograd.py @@ -0,0 +1,102 @@ +from . import variable, vector +import copapy.backend as cpb +from typing import Any, Sequence, overload +import copapy as cp +from ._basic_types import Net, unifloat + + +@overload +def grad(var: variable[Any], to: variable[Any]) -> unifloat: ... +@overload +def grad(var: variable[Any], to: Sequence[variable[Any]]) -> Sequence[unifloat]: ... +@overload +def grad(var: variable[Any], to: vector[Any]) -> vector[float]: ... +def grad(var: variable[Any], to: variable[Any] | Sequence[variable[Any]] | vector[Any]) -> unifloat | Sequence[unifloat] | vector[float]: + edges = cpb.get_all_dag_edges([var.source]) + ordered_ops = cpb.stable_toposort(edges) + + net_lookup = {net.source: net for node in ordered_ops for net in node.args} + grad_dict: dict[Net, unifloat] = dict() + + def add_grad(val: variable[Any], gradient_value: unifloat) -> None: + grad_dict[val] = grad_dict.get(val, 0.0) + gradient_value + + for node in reversed(ordered_ops): + print(f"--> {'x' if node in net_lookup else ' '}", node, f"{net_lookup.get(node)}") + if node.args: + args: Sequence[Any] = [v for v in node.args] + g = 1.0 if node is var.source else grad_dict[net_lookup[node]] + opn = node.name.split('_')[0] + x: variable[Any] = args[0] + y: variable[Any] = args[1] if len(args) > 1 else x + + if opn in ['ge', 'gt', 'eq', 'ne']: + pass # Derivative is 0 + + elif opn == 'add': + add_grad(x, g) + add_grad(y, g) + + elif opn == 'sub': + add_grad(x, g) + add_grad(y, -g) + + elif opn == 'mul': + add_grad(x, y * g) + add_grad(y, x * g) + + elif opn == 'div': + add_grad(x, g / y) + add_grad(y, -x * g / (y**2)) + + elif opn == 'pow': + add_grad(x, (y * (x ** (y - 1))) * g) + add_grad(y, (x ** y * cp.log(x)) * g) + + elif opn == 'sqrt': + add_grad(x, g * (0.5 / cp.sqrt(x))) + + elif opn == 'abs': + add_grad(x, g * cp.sign(x)) + + elif opn == 'sin': + add_grad(x, g * cp.cos(x)) + + elif opn == 'cos': + add_grad(x, g * -cp.sin(x)) + + elif opn == 'tan': + add_grad(x, g * (1 / cp.cos(x) ** 2)) + + elif opn == 'asin': + add_grad(x, g * (1 / cp.sqrt(1 - x**2))) + + elif opn == 'acos': + add_grad(x, g * (-1 / cp.sqrt(1 - x**2))) + + elif opn == 'atan': + add_grad(x, g * (1 / (1 + x**2))) + + elif opn == 'atan2': + denom = x**2 + y**2 + add_grad(x, g * (-y / denom)) + add_grad(y, g * ( x / denom)) + + elif opn == 'log': + add_grad(x, g / x) + + elif opn == 'exp': + add_grad(x, g * cp.exp(x)) + + elif opn == 'gt': + add_grad(x, g) + add_grad(y, -g) + + else: + raise ValueError(f"Operation {opn} not yet supported for auto diff.") + + if isinstance(to, variable): + return grad_dict[to] + if isinstance(to, vector): + return vector(grad_dict[dvar] for dvar in to) + return [grad_dict[dvar] for dvar in to] \ No newline at end of file From ebb4abc5d3177a9e430cb0040c609ee14a3f76d5 Mon Sep 17 00:00:00 2001 From: Nicolas Kruse Date: Thu, 4 Dec 2025 18:18:29 +0100 Subject: [PATCH 19/39] type hints revised --- src/copapy/_autograd.py | 140 ++++++++++++++++++++---------------- src/copapy/_basic_types.py | 4 +- src/copapy/_compiler.py | 6 +- src/copapy/_helper_types.py | 4 ++ src/copapy/_math.py | 34 +++++---- src/copapy/_matrices.py | 44 ++++++------ src/copapy/_vectors.py | 65 ++++++++++++++--- 7 files changed, 182 insertions(+), 115 deletions(-) create mode 100644 src/copapy/_helper_types.py diff --git a/src/copapy/_autograd.py b/src/copapy/_autograd.py index 613256d..685d9e4 100644 --- a/src/copapy/_autograd.py +++ b/src/copapy/_autograd.py @@ -1,4 +1,4 @@ -from . import variable, vector +from . import variable, vector, matrix import copapy.backend as cpb from typing import Any, Sequence, overload import copapy as cp @@ -6,13 +6,25 @@ from ._basic_types import Net, unifloat @overload -def grad(var: variable[Any], to: variable[Any]) -> unifloat: ... +def grad(x: variable[Any], y: variable[Any]) -> unifloat: ... @overload -def grad(var: variable[Any], to: Sequence[variable[Any]]) -> Sequence[unifloat]: ... +def grad(x: variable[Any], y: Sequence[variable[Any]]) -> list[unifloat]: ... @overload -def grad(var: variable[Any], to: vector[Any]) -> vector[float]: ... -def grad(var: variable[Any], to: variable[Any] | Sequence[variable[Any]] | vector[Any]) -> unifloat | Sequence[unifloat] | vector[float]: - edges = cpb.get_all_dag_edges([var.source]) +def grad(x: variable[Any], y: vector[Any]) -> vector[float]: ... +@overload +def grad(x: variable[Any], y: matrix[Any]) -> matrix[float]: ... +def grad(x: variable[Any], y: variable[Any] | Sequence[variable[Any]] | vector[Any] | matrix[float]) -> Any: + """Returns the partial derivative dx/dy where x needs to be a scalar + and y might be a scalar, a list of scalars, a vector or matrix. + + Arguments: + x: Value to return derivative of + y: Value(s) to derive in respect to + + Returns: + Derivative of x with the type and dimensions of y + """ + edges = cpb.get_all_dag_edges([x.source]) ordered_ops = cpb.stable_toposort(edges) net_lookup = {net.source: net for node in ordered_ops for net in node.args} @@ -24,79 +36,81 @@ def grad(var: variable[Any], to: variable[Any] | Sequence[variable[Any]] | vecto for node in reversed(ordered_ops): print(f"--> {'x' if node in net_lookup else ' '}", node, f"{net_lookup.get(node)}") if node.args: - args: Sequence[Any] = [v for v in node.args] - g = 1.0 if node is var.source else grad_dict[net_lookup[node]] + args: Sequence[Any] = list(node.args) + g = 1.0 if node is x.source else grad_dict[net_lookup[node]] opn = node.name.split('_')[0] - x: variable[Any] = args[0] - y: variable[Any] = args[1] if len(args) > 1 else x + a: variable[Any] = args[0] + b: variable[Any] = args[1] if len(args) > 1 else a - if opn in ['ge', 'gt', 'eq', 'ne']: - pass # Derivative is 0 + if opn in ['ge', 'gt', 'eq', 'ne', 'floordiv', 'bwand', 'bwor', 'bwxor']: + pass # Derivative is 0 for all ops returning integers elif opn == 'add': - add_grad(x, g) - add_grad(y, g) + add_grad(a, g) + add_grad(b, g) elif opn == 'sub': - add_grad(x, g) - add_grad(y, -g) + add_grad(a, g) + add_grad(b, -g) elif opn == 'mul': - add_grad(x, y * g) - add_grad(y, x * g) + add_grad(a, b * g) + add_grad(b, a * g) - elif opn == 'div': - add_grad(x, g / y) - add_grad(y, -x * g / (y**2)) + elif opn == 'div': + add_grad(a, g / b) + add_grad(b, -a * g / (b**2)) - elif opn == 'pow': - add_grad(x, (y * (x ** (y - 1))) * g) - add_grad(y, (x ** y * cp.log(x)) * g) - - elif opn == 'sqrt': - add_grad(x, g * (0.5 / cp.sqrt(x))) - - elif opn == 'abs': - add_grad(x, g * cp.sign(x)) - - elif opn == 'sin': - add_grad(x, g * cp.cos(x)) - - elif opn == 'cos': - add_grad(x, g * -cp.sin(x)) - - elif opn == 'tan': - add_grad(x, g * (1 / cp.cos(x) ** 2)) - - elif opn == 'asin': - add_grad(x, g * (1 / cp.sqrt(1 - x**2))) - - elif opn == 'acos': - add_grad(x, g * (-1 / cp.sqrt(1 - x**2))) - - elif opn == 'atan': - add_grad(x, g * (1 / (1 + x**2))) - - elif opn == 'atan2': - denom = x**2 + y**2 - add_grad(x, g * (-y / denom)) - add_grad(y, g * ( x / denom)) + elif opn == 'mod': + add_grad(a, g) + add_grad(b, -a * g / b) elif opn == 'log': - add_grad(x, g / x) + add_grad(a, g / a) elif opn == 'exp': - add_grad(x, g * cp.exp(x)) + add_grad(a, g * cp.exp(a)) - elif opn == 'gt': - add_grad(x, g) - add_grad(y, -g) + elif opn == 'pow': + add_grad(a, (b * (a ** (b - 1))) * g) + add_grad(b, (a ** b * cp.log(a)) * g) + + elif opn == 'sqrt': + add_grad(a, g * (0.5 / cp.sqrt(a))) + + #elif opn == 'abs': + # add_grad(x, g * cp.sign(x)) + + elif opn == 'sin': + add_grad(a, g * cp.cos(a)) + + elif opn == 'cos': + add_grad(a, g * -cp.sin(a)) + + elif opn == 'tan': + add_grad(a, g * (1 / cp.cos(a) ** 2)) + + elif opn == 'asin': + add_grad(a, g * (1 / cp.sqrt(1 - a**2))) + + elif opn == 'acos': + add_grad(a, g * (-1 / cp.sqrt(1 - a**2))) + + elif opn == 'atan': + add_grad(a, g * (1 / (1 + a**2))) + + elif opn == 'atan2': + denom = a**2 + b**2 + add_grad(a, g * (-b / denom)) + add_grad(b, g * ( a / denom)) else: raise ValueError(f"Operation {opn} not yet supported for auto diff.") - if isinstance(to, variable): - return grad_dict[to] - if isinstance(to, vector): - return vector(grad_dict[dvar] for dvar in to) - return [grad_dict[dvar] for dvar in to] \ No newline at end of file + if isinstance(y, variable): + return grad_dict[y] + if isinstance(y, vector): + return vector(grad_dict[yi] if isinstance(yi, variable) else 0.0 for yi in y) + if isinstance(y, matrix): + return matrix((grad_dict[yi] if isinstance(yi, variable) else 0.0 for yi in row) for row in y) + return [grad_dict[yi] for yi in y] diff --git a/src/copapy/_basic_types.py b/src/copapy/_basic_types.py index b32828c..879488a 100644 --- a/src/copapy/_basic_types.py +++ b/src/copapy/_basic_types.py @@ -2,13 +2,13 @@ import pkgutil from typing import Any, Sequence, TypeVar, overload, TypeAlias, Generic, cast from ._stencils import stencil_database, detect_process_arch import copapy as cp +from ._helper_types import TNum NumLike: TypeAlias = 'variable[int] | variable[float] | int | float' unifloat: TypeAlias = 'variable[float] | float' uniint: TypeAlias = 'variable[int] | int' TCPNum = TypeVar("TCPNum", bound='variable[Any]') -TNum = TypeVar("TNum", int, float) TVarNumb: TypeAlias = 'variable[Any] | int | float' stencil_cache: dict[tuple[str, str], stencil_database] = {} @@ -312,7 +312,7 @@ class variable(Generic[TNum], Net): return add_op('bwand', [self, other], True) def __rand__(self, other: uniint) -> 'variable[int]': - return add_op('rwand', [other, self], True) + return add_op('bwand', [other, self], True) def __or__(self, other: uniint) -> 'variable[int]': return add_op('bwor', [self, other], True) diff --git a/src/copapy/_compiler.py b/src/copapy/_compiler.py index e4b1553..a385874 100644 --- a/src/copapy/_compiler.py +++ b/src/copapy/_compiler.py @@ -65,7 +65,7 @@ def get_all_dag_edges(nodes: Iterable[Node]) -> Generator[tuple[Node, Node], Non Tuples of (source_node, target_node) representing edges in the DAG """ emitted_nodes: set[tuple[Node, Node]] = set() - + for node in nodes: yield from get_all_dag_edges(net.source for net in node.args) for net in node.args: @@ -138,7 +138,7 @@ def add_write_ops(net_node_list: list[tuple[Net | None, Node]], const_nets: list read_back_nets = { net for net, node in net_node_list if net and node.name.startswith('read_')} - + registers: list[Net | None] = [None, None] for net, node in net_node_list: @@ -253,7 +253,7 @@ def get_aux_func_layout(function_names: Iterable[str], sdb: stencil_database, of alignment = sdb.get_section_alignment(index) offset = (offset + alignment - 1) // alignment * alignment section_list.append((index, offset, lengths)) - section_cache[index] = offset + section_cache[index] = offset function_lookup[name] = offset + sdb.get_symbol_offset(name) offset += lengths diff --git a/src/copapy/_helper_types.py b/src/copapy/_helper_types.py new file mode 100644 index 0000000..e18eb57 --- /dev/null +++ b/src/copapy/_helper_types.py @@ -0,0 +1,4 @@ +from typing import TypeVar + +TNum = TypeVar("TNum", int, float) +U = TypeVar("U", int, float) diff --git a/src/copapy/_math.py b/src/copapy/_math.py index 6f775d5..03a22bf 100644 --- a/src/copapy/_math.py +++ b/src/copapy/_math.py @@ -2,12 +2,13 @@ from . import vector from ._vectors import VecNumLike from . import variable, NumLike from typing import TypeVar, Any, overload, Callable -from ._basic_types import add_op +from ._basic_types import add_op, unifloat import math T = TypeVar("T", int, float, variable[int], variable[float]) U = TypeVar("U", int, float) + @overload def exp(x: float | int) -> float: ... @overload @@ -284,7 +285,9 @@ def get_42(x: NumLike) -> variable[float] | float: def abs(x: U) -> U: ... @overload def abs(x: variable[U]) -> variable[U]: ... -def abs(x: U | variable[U]) -> Any: +@overload +def abs(x: vector[U]) -> vector[U]: ... +def abs(x: U | variable[U] | vector[U]) -> Any: """Absolute value function Arguments: @@ -293,16 +296,18 @@ def abs(x: U | variable[U]) -> Any: Returns: Absolute value of x """ + #tt = -x * (x < 0) ret = (x < 0) * -x + (x >= 0) * x return ret # REMpyright: ignore[reportReturnType] -#TODO: Add vector support @overload def sign(x: U) -> U: ... @overload def sign(x: variable[U]) -> variable[U]: ... -def sign(x: U | variable[U]) -> Any: +@overload +def sign(x: vector[U]) -> vector[U]: ... +def sign(x: U | variable[U] | vector[U]) -> Any: """Return 1 for positive numbers and -1 for negative numbers. For an input of 0 the return value is 0. @@ -333,13 +338,13 @@ def clamp(x: U | variable[U] | vector[U], min_value: U | variable[U], max_value: x: Input value min_value: Minimum limit max_value: Maximum limit - + Returns: Clamped value of x """ if isinstance(x, vector): return vector(clamp(comp, min_value, max_value) for comp in x.values) - + return (x < min_value) * min_value + \ (x > max_value) * max_value + \ ((x >= min_value) & (x <= max_value)) * x @@ -384,16 +389,16 @@ def max(x: U | variable[U], y: U | variable[U]) -> Any: @overload -def lerp(v1: variable[U], v2: U | variable[U], t: U | variable[U]) -> variable[U]: ... +def lerp(v1: variable[U], v2: U | variable[U], t: unifloat) -> variable[U]: ... @overload -def lerp(v1: U | variable[U], v2: variable[U], t: U | variable[U]) -> variable[U]: ... +def lerp(v1: U | variable[U], v2: variable[U], t: unifloat) -> variable[U]: ... @overload -def lerp(v1: U | variable[U], v2: U | variable[U], t: variable[U]) -> variable[U]: ... +def lerp(v1: U | variable[U], v2: U | variable[U], t: variable[float]) -> variable[U]: ... @overload -def lerp(v1: U, v2: U, t: U) -> U: ... +def lerp(v1: U, v2: U, t: float) -> U: ... @overload -def lerp(v1: vector[U], v2: vector[U], t: 'U | variable[U]') -> vector[U]: ... -def lerp(v1: U | variable[U] | vector[U], v2: U | variable[U] | vector[U], t: U | variable[U]) -> Any: +def lerp(v1: vector[U], v2: vector[U], t: unifloat) -> vector[U]: ... +def lerp(v1: U | variable[U] | vector[U], v2: U | variable[U] | vector[U], t: unifloat) -> Any: """Linearly interpolate between two values or vectors v1 and v2 by a factor t.""" if isinstance(v1, vector) or isinstance(v2, vector): assert isinstance(v1, vector) and isinstance(v2, vector), "None or both v1 and v2 must be vectors." @@ -402,12 +407,13 @@ def lerp(v1: U | variable[U] | vector[U], v2: U | variable[U] | vector[U], t: U return v1 * (1 - t) + v2 * t -#TODO: Add vector support @overload def relu(x: U) -> U: ... @overload def relu(x: variable[U]) -> variable[U]: ... -def relu(x: U | variable[U]) -> Any: +@overload +def relu(x: vector[U]) -> vector[U]: ... +def relu(x: U | variable[U] | vector[U]) -> Any: """Returns x for x > 0 and otherwise 0.""" ret = (x > 0) * x return ret diff --git a/src/copapy/_matrices.py b/src/copapy/_matrices.py index 92528c4..f1a28c2 100644 --- a/src/copapy/_matrices.py +++ b/src/copapy/_matrices.py @@ -1,19 +1,19 @@ from . import variable from ._vectors import vector from ._mixed import mixed_sum -from typing import Generic, TypeVar, Iterable, Any, overload, TypeAlias, Callable, Iterator +from typing import TypeVar, Iterable, Any, overload, TypeAlias, Callable, Iterator, Generic +from ._helper_types import TNum MatNumLike: TypeAlias = 'matrix[int] | matrix[float] | variable[int] | variable[float] | int | float' MatIntLike: TypeAlias = 'matrix[int] | variable[int] | int' MatFloatLike: TypeAlias = 'matrix[float] | variable[float] | float' -TT = TypeVar("TT", int, float) U = TypeVar("U", int, float) -class matrix(Generic[TT]): +class matrix(Generic[TNum]): """Mathematical matrix class supporting basic operations and interactions with variables. """ - def __init__(self, values: Iterable[Iterable[TT | variable[TT]]]): + def __init__(self, values: Iterable[Iterable[TNum | variable[TNum]]]): """Create a matrix with given values and variables. Args: @@ -23,7 +23,7 @@ class matrix(Generic[TT]): if rows: row_len = len(rows[0]) assert all(len(row) == row_len for row in rows), "All rows must have the same length" - self.values: tuple[tuple[variable[TT] | TT, ...], ...] = tuple(rows) + self.values: tuple[tuple[variable[TNum] | TNum, ...], ...] = tuple(rows) self.rows = len(self.values) self.cols = len(self.values[0]) if self.values else 0 @@ -33,13 +33,13 @@ class matrix(Generic[TT]): def __len__(self) -> int: return self.rows - def __getitem__(self, index: int) -> tuple[variable[TT] | TT, ...]: + def __getitem__(self, index: int) -> tuple[variable[TNum] | TNum, ...]: return self.values[index] - def __iter__(self) -> Iterator[tuple[variable[TT] | TT, ...]]: + def __iter__(self) -> Iterator[tuple[variable[TNum] | TNum, ...]]: return iter(self.values) - def __neg__(self) -> 'matrix[TT]': + def __neg__(self) -> 'matrix[TNum]': return matrix((-a for a in row) for row in self.values) @overload @@ -165,23 +165,23 @@ class matrix(Generic[TT]): ) @overload - def __matmul__(self: 'matrix[TT]', other: 'vector[TT]') -> 'vector[TT]': ... + def __matmul__(self: 'matrix[TNum]', other: 'vector[TNum]') -> 'vector[TNum]': ... @overload - def __matmul__(self: 'matrix[TT]', other: 'matrix[TT]') -> 'matrix[TT]': ... - def __matmul__(self: 'matrix[TT]', other: 'matrix[TT] | vector[TT]') -> 'matrix[TT] | vector[TT]': + def __matmul__(self: 'matrix[TNum]', other: 'matrix[TNum]') -> 'matrix[TNum]': ... + def __matmul__(self: 'matrix[TNum]', other: 'matrix[TNum] | vector[TNum]') -> 'matrix[TNum] | vector[TNum]': """Matrix multiplication using @ operator""" if isinstance(other, vector): assert self.cols == len(other.values), \ f"Matrix columns ({self.cols}) must match vector length ({len(other.values)})" vec_result = (mixed_sum(a * b for a, b in zip(row, other.values)) for row in self.values) return vector(vec_result) - else: + else: assert isinstance(other, matrix), "Cannot multiply matrix with {type(other)}" assert self.cols == other.rows, \ f"Matrix columns ({self.cols}) must match other matrix rows ({other.rows})" - result: list[list[TT | variable[TT]]] = [] + result: list[list[TNum | variable[TNum]]] = [] for row in self.values: - new_row: list[TT | variable[TT]] = [] + new_row: list[TNum | variable[TNum]] = [] for col_idx in range(other.cols): col = tuple(other.values[i][col_idx] for i in range(other.rows)) element = sum(a * b for a, b in zip(row, col)) @@ -189,7 +189,7 @@ class matrix(Generic[TT]): result.append(new_row) return matrix(result) - def transpose(self) -> 'matrix[TT]': + def transpose(self) -> 'matrix[TNum]': """Return the transpose of the matrix.""" if not self.values: return matrix([]) @@ -197,23 +197,23 @@ class matrix(Generic[TT]): tuple(self.values[i][j] for i in range(self.rows)) for j in range(self.cols) ) - + @property - def T(self) -> 'matrix[TT]': + def T(self) -> 'matrix[TNum]': return self.transpose() - def row(self, index: int) -> vector[TT]: + def row(self, index: int) -> vector[TNum]: """Get a row as a vector.""" assert 0 <= index < self.rows, f"Row index {index} out of bounds" return vector(self.values[index]) - def col(self, index: int) -> vector[TT]: + def col(self, index: int) -> vector[TNum]: """Get a column as a vector.""" assert 0 <= index < self.cols, f"Column index {index} out of bounds" return vector(self.values[i][index] for i in range(self.rows)) @overload - def trace(self: 'matrix[TT]') -> TT | variable[TT]: ... + def trace(self: 'matrix[TNum]') -> TNum | variable[TNum]: ... @overload def trace(self: 'matrix[int]') -> int | variable[int]: ... @overload @@ -224,7 +224,7 @@ class matrix(Generic[TT]): return mixed_sum(self.values[i][i] for i in range(self.rows)) @overload - def sum(self: 'matrix[TT]') -> TT | variable[TT]: ... + def sum(self: 'matrix[TNum]') -> TNum | variable[TNum]: ... @overload def sum(self: 'matrix[int]') -> int | variable[int]: ... @overload @@ -240,7 +240,7 @@ class matrix(Generic[TT]): for row in self.values ) - def homogenize(self) -> 'matrix[TT]': + def homogenize(self) -> 'matrix[TNum]': """Convert all elements to variables if any element is a variable.""" if any(isinstance(val, variable) for row in self.values for val in row): return matrix( diff --git a/src/copapy/_vectors.py b/src/copapy/_vectors.py index e6fbf40..f6baee4 100644 --- a/src/copapy/_vectors.py +++ b/src/copapy/_vectors.py @@ -1,27 +1,28 @@ from . import variable from ._mixed import mixed_sum, mixed_homogenize -from typing import Generic, TypeVar, Iterable, Any, overload, TypeAlias, Callable, Iterator +from typing import TypeVar, Iterable, Any, overload, TypeAlias, Callable, Iterator, Generic import copapy as cp +from ._helper_types import TNum -VecNumLike: TypeAlias = 'vector[int] | vector[float] | variable[int] | variable[float] | int | float | bool' +#VecNumLike: TypeAlias = 'vector[int] | vector[float] | variable[int] | variable[float] | int | float | bool' +VecNumLike: TypeAlias = 'vector[Any] | variable[Any] | int | float | bool' VecIntLike: TypeAlias = 'vector[int] | variable[int] | int' VecFloatLike: TypeAlias = 'vector[float] | variable[float] | float' -T = TypeVar("T", int, float) U = TypeVar("U", int, float) epsilon = 1e-20 -class vector(Generic[T]): +class vector(Generic[TNum]): """Mathematical vector class supporting basic operations and interactions with variables. """ - def __init__(self, values: Iterable[T | variable[T]]): + def __init__(self, values: Iterable[TNum | variable[TNum]]): """Create a vector with given values and variables. Args: values: iterable of constant values and variables """ - self.values: tuple[variable[T] | T, ...] = tuple(values) + self.values: tuple[variable[TNum] | TNum, ...] = tuple(values) def __repr__(self) -> str: return f"vector({self.values})" @@ -29,13 +30,13 @@ class vector(Generic[T]): def __len__(self) -> int: return len(self.values) - def __getitem__(self, index: int) -> variable[T] | T: + def __getitem__(self, index: int) -> variable[TNum] | TNum: return self.values[index] - def __neg__(self) -> 'vector[T]': + def __neg__(self) -> 'vector[TNum]': return vector(-a for a in self.values) - def __iter__(self) -> Iterator[variable[T] | T]: + def __iter__(self) -> Iterator[variable[TNum] | TNum]: return iter(self.values) @overload @@ -56,6 +57,8 @@ class vector(Generic[T]): def __radd__(self: 'vector[float]', other: VecNumLike) -> 'vector[float]': ... @overload def __radd__(self: 'vector[int]', other: variable[int] | int) -> 'vector[int]': ... + @overload + def __radd__(self, other: VecNumLike) -> 'vector[Any]': ... def __radd__(self, other: Any) -> Any: return self + other @@ -77,6 +80,8 @@ class vector(Generic[T]): def __rsub__(self: 'vector[float]', other: VecNumLike) -> 'vector[float]': ... @overload def __rsub__(self: 'vector[int]', other: variable[int] | int) -> 'vector[int]': ... + @overload + def __rsub__(self, other: VecNumLike) -> 'vector[Any]': ... def __rsub__(self, other: VecNumLike) -> Any: if isinstance(other, vector): assert len(self.values) == len(other.values) @@ -101,6 +106,8 @@ class vector(Generic[T]): def __rmul__(self: 'vector[float]', other: VecNumLike) -> 'vector[float]': ... @overload def __rmul__(self: 'vector[int]', other: variable[int] | int) -> 'vector[int]': ... + @overload + def __rmul__(self, other: VecNumLike) -> 'vector[Any]': ... def __rmul__(self, other: VecNumLike) -> Any: return self * other @@ -150,6 +157,42 @@ class vector(Generic[T]): a3 * b1 - a1 * b3, a1 * b2 - a2 * b1 ]) + + def __gt__(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) + + 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) + + 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) + + 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) + + 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 __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) @overload def sum(self: 'vector[int]') -> int | variable[int]: ... @@ -168,8 +211,8 @@ class vector(Generic[T]): """Returns a normalized (unit length) version of the vector.""" mag = self.magnitude() + epsilon return self / mag - - def homogenize(self) -> 'vector[T]': + + def homogenize(self) -> 'vector[TNum]': if any(isinstance(val, variable) for val in self.values): return vector(mixed_homogenize(self)) else: From 5daa54fafb75695f191a3f2be405fae93b432641 Mon Sep 17 00:00:00 2001 From: Nicolas Kruse Date: Thu, 4 Dec 2025 18:19:33 +0100 Subject: [PATCH 20/39] code style adjustments --- src/copapy/_stencils.py | 8 +++--- src/copapy/_target.py | 6 ++--- src/copapy/filters.py | 7 +++--- tests/test_ast_gen.py | 12 ++++----- tests/test_comp_timing.py | 16 ++++++------ tests/test_matrix.py | 52 +++++++++++++++++++-------------------- tests/test_ops_aarch64.py | 4 +-- tests/test_ops_armv7.py | 4 +-- tests/test_ops_x86.py | 20 +++++++-------- 9 files changed, 65 insertions(+), 64 deletions(-) diff --git a/src/copapy/_stencils.py b/src/copapy/_stencils.py index 3c74561..75d16f0 100644 --- a/src/copapy/_stencils.py +++ b/src/copapy/_stencils.py @@ -167,7 +167,7 @@ class stencil_database(): # cache miss: cache: list[relocation_entry] = [] self._relocation_cache[cache_key] = cache - + symbol = self.elf.symbols[symbol_name] if stencil: start_index, end_index = get_stencil_position(symbol) @@ -353,11 +353,11 @@ class stencil_database(): def get_symbol_size(self, name: str) -> int: """Returns the size of a specified symbol name.""" return self.elf.symbols[name].fields['st_size'] - + def get_symbol_offset(self, name: str) -> int: """Returns the offset of a specified symbol in the section.""" return self.elf.symbols[name].fields['st_value'] - + def get_symbol_section_index(self, name: str) -> int: """Returns the section index for a specified symbol name.""" return self.elf.symbols[name].fields['st_shndx'] @@ -365,7 +365,7 @@ class stencil_database(): def get_section_size(self, index: int) -> int: """Returns the size of a section specified by index.""" return self.elf.sections[index].fields['sh_size'] - + def get_section_alignment(self, index: int) -> int: """Returns the required alignment of a section specified by index.""" return self.elf.sections[index].fields['sh_addralign'] diff --git a/src/copapy/_target.py b/src/copapy/_target.py index 6039ce5..13fe4f6 100644 --- a/src/copapy/_target.py +++ b/src/copapy/_target.py @@ -75,11 +75,11 @@ class Target(): """ if isinstance(net, Iterable): return [self.read_value(ni) if isinstance(ni, variable) else ni for ni in net] - + if isinstance(net, float | int): - print(f"Warning: value is not a copypy value") + print("Warning: value is not a copypy value") return net - + assert isinstance(net, Net), "Variable must be a copapy variable object" assert net in self._variables, f"Variable {net} not found. It might not have been compiled for the target." addr, lengths, var_type = self._variables[net] diff --git a/src/copapy/filters.py b/src/copapy/filters.py index 8639b1f..abd6d44 100644 --- a/src/copapy/filters.py +++ b/src/copapy/filters.py @@ -1,5 +1,6 @@ from . import variable, vector -from ._basic_types import iif, unifloat, TNum +from ._basic_types import iif, unifloat +from._helper_types import TNum from typing import Any, Iterable @@ -18,7 +19,7 @@ def _inv_argsort(input_vector: vector[TNum]) -> vector[int]: def argsort(input_vector: vector[TNum]) -> vector[int]: """ - Perform an indirect sort. It returns an array of indices that index data + Perform an indirect sort. It returns an array of indices that index data in sorted order. Args: @@ -61,4 +62,4 @@ def mean(input_vector: vector[Any]) -> unifloat: Returns: unifloat: The mean value of the input vector. """ - return input_vector.sum() / len(input_vector) \ No newline at end of file + return input_vector.sum() / len(input_vector) diff --git a/tests/test_ast_gen.py b/tests/test_ast_gen.py index 0c3ebb0..89ee086 100644 --- a/tests/test_ast_gen.py +++ b/tests/test_ast_gen.py @@ -1,6 +1,6 @@ from copapy import variable from copapy.backend import Write -import copapy.backend as cpbe +import copapy.backend as cpb def test_ast_generation(): @@ -33,27 +33,27 @@ def test_ast_generation(): print(out) print('-- get_edges:') - edges = list(cpbe.get_all_dag_edges(out)) + edges = list(cpb.get_all_dag_edges(out)) for p in edges: print('#', p) print('-- get_ordered_ops:') - ordered_ops = list(cpbe.stable_toposort(edges)) + ordered_ops = cpb.stable_toposort(edges) for p in ordered_ops: print('#', p) print('-- get_consts:') - const_list = cpbe.get_const_nets(ordered_ops) + const_list = cpb.get_const_nets(ordered_ops) for p in const_list: print('#', p) print('-- add_read_ops:') - output_ops = list(cpbe.add_read_ops(ordered_ops)) + output_ops = list(cpb.add_read_ops(ordered_ops)) for p in output_ops: print('#', p) print('-- add_write_ops:') - extended_output_ops = list(cpbe.add_write_ops(output_ops, const_list)) + extended_output_ops = list(cpb.add_write_ops(output_ops, const_list)) for p in extended_output_ops: print('#', p) print('--') diff --git a/tests/test_comp_timing.py b/tests/test_comp_timing.py index 9dc52b0..301851e 100644 --- a/tests/test_comp_timing.py +++ b/tests/test_comp_timing.py @@ -1,16 +1,16 @@ import time -from copapy import variable from copapy import backend from copapy.backend import Write, stencil_db_from_package -import copapy.backend as cpbe +import copapy.backend as cpb import copapy as cp import copapy._binwrite as binw from copapy._compiler import get_nets, get_section_layout, get_data_layout from copapy._compiler import patch_entry, CPConstant, get_aux_func_layout + def test_timing_compiler(): t1 = cp.vector([10, 11]*128) + cp.vector(cp.variable(v) for v in range(256)) - t2 = t1.sum() + #t2 = t1.sum() t3 = cp.vector(cp.variable(1 / (v + 1)) for v in range(256)) t5 = ((t3 * t1) * 2).magnitude() out = [Write(t5)] @@ -19,7 +19,7 @@ def test_timing_compiler(): print('-- get_edges:') t0 = time.time() - edges = list(cpbe.get_all_dag_edges(out)) + edges = list(cpb.get_all_dag_edges(out)) t1 = time.time() print(f' found {len(edges)} edges') #for p in edges: @@ -28,7 +28,7 @@ def test_timing_compiler(): print('-- get_ordered_ops:') t0 = time.time() - ordered_ops = list(cpbe.stable_toposort(edges)) + ordered_ops = cpb.stable_toposort(edges) t1 = time.time() print(f' found {len(ordered_ops)} ops') #for p in ordered_ops: @@ -37,7 +37,7 @@ def test_timing_compiler(): print('-- get_consts:') t0 = time.time() - const_net_list = cpbe.get_const_nets(ordered_ops) + const_net_list = cpb.get_const_nets(ordered_ops) t1 = time.time() #for p in const_list: # print('#', p) @@ -45,7 +45,7 @@ def test_timing_compiler(): print('-- add_read_ops:') t0 = time.time() - output_ops = list(cpbe.add_read_ops(ordered_ops)) + output_ops = list(cpb.add_read_ops(ordered_ops)) t1 = time.time() #for p in output_ops: # print('#', p) @@ -53,7 +53,7 @@ def test_timing_compiler(): print('-- add_write_ops:') t0 = time.time() - extended_output_ops = list(cpbe.add_write_ops(output_ops, const_net_list)) + extended_output_ops = list(cpb.add_write_ops(output_ops, const_net_list)) t1 = time.time() #for p in extended_output_ops: # print('#', p) diff --git a/tests/test_matrix.py b/tests/test_matrix.py index 2930f34..324b481 100644 --- a/tests/test_matrix.py +++ b/tests/test_matrix.py @@ -25,7 +25,7 @@ def test_matrix_addition(): m1 = cp.matrix([[1, 2], [3, 4]]) m2 = cp.matrix([[5, 6], [7, 8]]) m3 = m1 + m2 - + assert m3[0] == (6, 8) assert m3[1] == (10, 12) @@ -34,7 +34,7 @@ def test_matrix_scalar_addition(): """Test matrix addition with scalar""" m1 = cp.matrix([[1, 2], [3, 4]]) m2 = m1 + 5 - + assert m2[0] == (6, 7) assert m2[1] == (8, 9) @@ -44,7 +44,7 @@ def test_matrix_subtraction(): m1 = cp.matrix([[5, 6], [7, 8]]) m2 = cp.matrix([[1, 2], [3, 4]]) m3 = m1 - m2 - + assert m3[0] == (4, 4) assert m3[1] == (4, 4) @@ -53,7 +53,7 @@ def test_matrix_scalar_subtraction(): """Test matrix subtraction with scalar""" m1 = cp.matrix([[5, 6], [7, 8]]) m2 = m1 - 2 - + assert m2[0] == (3, 4) assert m2[1] == (5, 6) @@ -62,7 +62,7 @@ def test_matrix_negation(): """Test matrix negation""" m1 = cp.matrix([[1, 2], [3, 4]]) m2 = -m1 - + assert m2[0] == (-1, -2) assert m2[1] == (-3, -4) @@ -72,7 +72,7 @@ def test_matrix_element_wise_multiplication(): m1 = cp.matrix([[1, 2], [3, 4]]) m2 = cp.matrix([[5, 6], [7, 8]]) m3 = m1 * m2 - + assert m3[0] == (5, 12) assert m3[1] == (21, 32) @@ -81,7 +81,7 @@ def test_matrix_scalar_multiplication(): """Test matrix multiplication with scalar""" m1 = cp.matrix([[1, 2], [3, 4]]) m2 = m1 * 3 - + assert m2[0] == (3, 6) assert m2[1] == (9, 12) @@ -91,7 +91,7 @@ def test_matrix_element_wise_division(): m1 = cp.matrix([[6.0, 8.0], [12.0, 16.0]]) m2 = cp.matrix([[2.0, 2.0], [3.0, 4.0]]) m3 = m1 / m2 - + assert m3[0][0] == pytest.approx(3.0) # pyright: ignore[reportUnknownMemberType] assert m3[0][1] == pytest.approx(4.0) # pyright: ignore[reportUnknownMemberType] assert m3[1][0] == pytest.approx(4.0) # pyright: ignore[reportUnknownMemberType] @@ -102,7 +102,7 @@ def test_matrix_scalar_division(): """Test matrix division by scalar""" 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] @@ -112,7 +112,7 @@ def test_matrix_vector_multiplication(): m = cp.matrix([[1, 2, 3], [4, 5, 6]]) v = cp.vector([7, 8, 9]) result = m @ v - + assert isinstance(result, cp.vector) assert len(result.values) == 2 assert result.values[0] == 1*7 + 2*8 + 3*9 @@ -124,7 +124,7 @@ def test_matrix_matrix_multiplication(): m1 = cp.matrix([[1, 2], [3, 4]]) m2 = cp.matrix([[5, 6], [7, 8]]) result = m1 @ m2 - + assert isinstance(result, cp.matrix) assert result.rows == 2 assert result.cols == 2 @@ -138,7 +138,7 @@ def test_matrix_transpose(): """Test matrix transpose""" m = cp.matrix([[1, 2, 3], [4, 5, 6]]) mt = m.transpose() - + assert mt.rows == 3 assert mt.cols == 2 assert mt[0] == (1, 4) @@ -150,7 +150,7 @@ def test_matrix_transpose_property(): """Test matrix transpose using .T property""" m = cp.matrix([[1, 2, 3], [4, 5, 6]]) mt = m.T - + assert mt.rows == 3 assert mt.cols == 2 assert mt[0] == (1, 4) @@ -160,7 +160,7 @@ def test_matrix_row_access(): """Test getting a row as a vector""" m = cp.matrix([[1, 2, 3], [4, 5, 6]]) row0 = m.row(0) - + assert isinstance(row0, cp.vector) assert row0.values == (1, 2, 3) @@ -169,7 +169,7 @@ def test_matrix_col_access(): """Test getting a column as a vector""" m = cp.matrix([[1, 2, 3], [4, 5, 6]]) col1 = m.col(1) - + assert isinstance(col1, cp.vector) assert col1.values == (2, 5) @@ -178,7 +178,7 @@ def test_matrix_trace(): """Test matrix trace (sum of diagonal elements)""" m = cp.matrix([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) trace = m.trace() - + assert trace == 1 + 5 + 9 @@ -194,7 +194,7 @@ def test_matrix_map(): """Test mapping a function over matrix elements""" m = cp.matrix([[1, 2], [3, 4]]) m_doubled = m.map(lambda x: x * 2) - + assert m_doubled[0] == (2, 4) assert m_doubled[1] == (6, 8) @@ -203,7 +203,7 @@ def test_matrix_homogenize(): """Test homogenizing matrix (converting to all variables)""" m = cp.matrix([[1, cp.variable(2)], [3, 4]]) m_homo = m.homogenize() - + for row in m_homo: for elem in row: assert isinstance(elem, cp.variable) @@ -212,7 +212,7 @@ def test_matrix_homogenize(): def test_identity_matrix(): """Test identity matrix creation""" m = cp.identity(3) - + assert m.rows == 3 assert m.cols == 3 assert m[0] == (1, 0, 0) @@ -223,7 +223,7 @@ def test_identity_matrix(): def test_zeros_matrix(): """Test zeros matrix creation""" m = cp.zeros(2, 3) - + assert m.rows == 2 assert m.cols == 3 assert m[0] == (0, 0, 0) @@ -233,7 +233,7 @@ def test_zeros_matrix(): def test_ones_matrix(): """Test ones matrix creation""" m = cp.ones(2, 3) - + assert m.rows == 2 assert m.cols == 3 assert m[0] == (1, 1, 1) @@ -244,7 +244,7 @@ def test_diagonal_matrix(): """Test diagonal matrix creation from vector""" v = cp.vector([1, 2, 3]) m = cp.diagonal(v) - + assert m.rows == 3 assert m.cols == 3 assert m[0] == (1, 0, 0) @@ -257,17 +257,17 @@ def test_matrix_with_variables_compiled(): m = cp.matrix([[cp.variable(1), 2], [3, cp.variable(4)]]) v = cp.vector([cp.variable(5), 6]) result = m @ v - + # result[0] = 1*5 + 2*6 = 17 # result[1] = 3*5 + 4*6 = 39 - + tg = cp.Target() tg.compile(result) tg.run() - + assert tg.read_value(result.values[0]) == pytest.approx(17) # pyright: ignore[reportUnknownMemberType] assert tg.read_value(result.values[1]) == pytest.approx(39) # pyright: ignore[reportUnknownMemberType] if __name__ == "__main__": - pytest.main([__file__, "-v"]) \ No newline at end of file + pytest.main([__file__, "-v"]) diff --git a/tests/test_ops_aarch64.py b/tests/test_ops_aarch64.py index e912edf..017def3 100644 --- a/tests/test_ops_aarch64.py +++ b/tests/test_ops_aarch64.py @@ -42,7 +42,7 @@ def check_for_qemu() -> bool: command = qemu_command + ['--version'] try: result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False) - except: + except Exception: return False return result.returncode == 0 @@ -128,7 +128,7 @@ def test_compile(): if not os.path.isfile('build/runner/coparun-aarch64'): warnings.warn("aarch64 runner not found, aarch64 test skipped!", UserWarning) return - + command = qemu_command + ['build/runner/coparun-aarch64', 'build/runner/test-arm64.copapy'] + ['build/runner/test-arm64.copapy.bin'] #try: result = run_command(command) diff --git a/tests/test_ops_armv7.py b/tests/test_ops_armv7.py index 1b53ca9..4941f9b 100644 --- a/tests/test_ops_armv7.py +++ b/tests/test_ops_armv7.py @@ -44,7 +44,7 @@ def check_for_qemu() -> bool: command = qemu_command + ['--version'] try: result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False) - except: + except Exception: return False return result.returncode == 0 @@ -130,7 +130,7 @@ def test_compile(): if not os.path.isfile('build/runner/coparun-armv7'): warnings.warn("armv7 runner not found, armv7 test skipped!", UserWarning) return - + command = qemu_command + ['build/runner/coparun-armv7', 'build/runner/test-armv7.copapy'] + ['build/runner/test-armv7.copapy.bin'] #try: result = run_command(command) diff --git a/tests/test_ops_x86.py b/tests/test_ops_x86.py index 29f2c96..069f341 100644 --- a/tests/test_ops_x86.py +++ b/tests/test_ops_x86.py @@ -1,4 +1,4 @@ -from copapy import NumLike, iif, variable, sin +from copapy import NumLike, iif, variable from copapy.backend import Write, compile_to_dag, add_read_command import subprocess from copapy import _binwrite @@ -70,16 +70,16 @@ def iiftests(c1: NumLike) -> list[NumLike]: @pytest.mark.runner def test_compile(): - t1 = cp.vector([10, 11, 12]) + cp.vector(cp.variable(v) for v in range(3)) - t2 = t1.sum() + #t1 = cp.vector([10, 11, 12]) + cp.vector(cp.variable(v) for v in range(3)) + #t2 = t1.sum() - t3 = cp.vector(cp.variable(1 / (v + 1)) for v in range(3)) - t4 = ((t3 * t1) * 2).sum() - t5 = ((t3 * t1) * 2).magnitude() + #t3 = cp.vector(cp.variable(1 / (v + 1)) for v in range(3)) + #t4 = ((t3 * t1) * 2).sum() + #t5 = ((t3 * t1) * 2).magnitude() c_i = variable(9) - c_f = variable(1.111) - c_b = variable(True) + #c_f = variable(1.111) + #c_b = variable(True) #ret_test = function1(c_i) + function1(c_f) + function2(c_i) + function2(c_f) + function3(c_i) + function4(c_i) + function5(c_b) + [c_i % 2, sin(c_f)] + iiftests(c_i) + iiftests(c_f) #ret_ref = function1(9) + function1(1.111) + function2(9) + function2(1.111) + function3(9) + function4(9) + function5(True) + [9 % 2, sin(1.111)] + iiftests(9) + iiftests(1.111) @@ -104,7 +104,7 @@ def test_compile(): ret_test = [cp.get_42(c_i)] ret_ref = [cp.get_42(9)] - out = [Write(r) for r in ret_test] + out = [Write(r) for r in ret_test] #ret_test += [c_i, v2] #ret_ref += [9, 4.44, -4.44] @@ -143,7 +143,7 @@ def test_compile(): try: result = run_command(command) except FileNotFoundError: - warnings.warn(f"Test skipped, executable not found.", UserWarning) + warnings.warn("Test skipped, executable not found.", UserWarning) return print('* Output from runner:\n--') From 257fe96bb373255bccc41d06c5dc9b1d0bf36e69 Mon Sep 17 00:00:00 2001 From: Nicolas Kruse Date: Thu, 4 Dec 2025 18:19:43 +0100 Subject: [PATCH 21/39] auto gfrad test added --- tests/test_autograd.py | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 tests/test_autograd.py diff --git a/tests/test_autograd.py b/tests/test_autograd.py new file mode 100644 index 0000000..f8ecaf1 --- /dev/null +++ b/tests/test_autograd.py @@ -0,0 +1,38 @@ +from copapy import variable, grad +import copapy as cp +import pytest + + +def test_autograd(): + # Validate against micrograd results from Andrej Karpathy + # https://github.com/karpathy/micrograd/blob/master/test/test_engine.py + a = variable(-4.0) + b = variable(2.0) + c = a + b + d = a * b + b**3 + c += c + 1 + c += 1 + c + (-a) + d += d * 2 + cp.relu(b + a) + d += 3 * d + cp.relu(b - a) + e = c - d + f = e**2 + g = f / 2.0 + g += 10.0 / f + + dg = grad(g, (a, b)) + + tg = cp.Target() + tg.compile(g, dg) + tg.run() + + + print(f"g = {tg.read_value(g)}") + print(f"dg/da = {tg.read_value(dg[0])} grad:{dg[0]} val:{a} = {tg.read_value(a)}") + print(f"dg/db = {tg.read_value(dg[1])} grad:{dg[1]} val:{b} = {tg.read_value(b)}") + + assert pytest.approx(dg[0], abs=1e-4) == 138.83381 # pyright: ignore[reportUnknownMemberType] + assert pytest.approx(dg[1], abs=1e-4) == 645.57725 # pyright: ignore[reportUnknownMemberType] + + +if __name__ == "__main__": + test_autograd() From da92aa9e2cbe26f68cad3e06d8e5967e28f79071 Mon Sep 17 00:00:00 2001 From: Nicolas Kruse Date: Thu, 4 Dec 2025 22:38:52 +0100 Subject: [PATCH 22/39] eye function added for creating matrices --- src/copapy/__init__.py | 5 +++-- src/copapy/_matrices.py | 25 +++++++++++++++++++------ src/copapy/_vectors.py | 2 -- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/src/copapy/__init__.py b/src/copapy/__init__.py index b8216c6..1309d5f 100644 --- a/src/copapy/__init__.py +++ b/src/copapy/__init__.py @@ -1,7 +1,7 @@ from ._target import Target from ._basic_types import NumLike, variable, generic_sdb, iif from ._vectors import vector, distance, scalar_projection, angle_between, rotate_vector, vector_projection -from ._matrices import matrix, identity, zeros, ones, diagonal +from ._matrices import matrix, identity, zeros, ones, diagonal, eye from ._math import sqrt, abs, sign, sin, cos, tan, asin, acos, atan, atan2, log, exp, pow, get_42, clamp, min, max, relu from ._autograd import grad @@ -40,5 +40,6 @@ __all__ = [ "angle_between", "rotate_vector", "vector_projection", - "grad" + "grad", + "eye" ] diff --git a/src/copapy/_matrices.py b/src/copapy/_matrices.py index f1a28c2..3086a8c 100644 --- a/src/copapy/_matrices.py +++ b/src/copapy/_matrices.py @@ -13,13 +13,16 @@ U = TypeVar("U", int, float) class matrix(Generic[TNum]): """Mathematical matrix class supporting basic operations and interactions with variables. """ - def __init__(self, values: Iterable[Iterable[TNum | variable[TNum]]]): + def __init__(self, values: Iterable[Iterable[TNum | variable[TNum]]] | vector[TNum]): """Create a matrix with given values and variables. Args: values: iterable of iterable of constant values and variables """ - rows = tuple(tuple(row) for row in values) + if isinstance(values, vector): + rows = (values.values,) + else: + rows = tuple(tuple(row) for row in values) if rows: row_len = len(rows[0]) assert all(len(row) == row_len for row in rows), "All rows must have the same length" @@ -33,8 +36,11 @@ class matrix(Generic[TNum]): def __len__(self) -> int: return self.rows - def __getitem__(self, index: int) -> tuple[variable[TNum] | TNum, ...]: - return self.values[index] + def __getitem__(self, key: tuple[int, int]) -> variable[TNum] | TNum: + assert len(key) == 2 + row = key[0] + col = key[1] + return self.values[row][col] def __iter__(self) -> Iterator[tuple[variable[TNum] | TNum, ...]]: return iter(self.values) @@ -251,8 +257,6 @@ class matrix(Generic[TNum]): return self -# Utility functions for matrices - def identity(size: int) -> matrix[int]: """Create an identity matrix of given size.""" return matrix( @@ -277,6 +281,15 @@ def ones(rows: int, cols: int) -> matrix[int]: ) +def eye(rows: int, cols: int | None = None) -> matrix[int]: + """Create a matrix with ones on the diagonal and zeros elsewhere.""" + cols = cols if cols else rows + return matrix( + tuple(1 if i == j else 0 for j in range(cols)) + for i in range(rows) + ) + + @overload def diagonal(vec: 'vector[int]') -> matrix[int]: ... @overload diff --git a/src/copapy/_vectors.py b/src/copapy/_vectors.py index f6baee4..4188d6d 100644 --- a/src/copapy/_vectors.py +++ b/src/copapy/_vectors.py @@ -223,8 +223,6 @@ class vector(Generic[TNum]): return vector(func(x) for x in self.values) -# Utility functions for 3D vectors with two arguments - def cross_product(v1: vector[float], v2: vector[float]) -> vector[float]: """Calculate the cross product of two 3D vectors.""" return v1.cross(v2) From 6d47779c030e016b43715d72ba87a4b7a392eeb1 Mon Sep 17 00:00:00 2001 From: Nicolas Kruse Date: Thu, 4 Dec 2025 22:39:12 +0100 Subject: [PATCH 23/39] benchmark script updated --- .gitignore | 1 + tests/benchmark.py | 237 +++++++++++++++++++++++++++++++++++++-------- 2 files changed, 196 insertions(+), 42 deletions(-) diff --git a/.gitignore b/.gitignore index 85770e0..f6a7942 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,4 @@ build/* /*.obj /src/*.pyd vc140.pdb +benchmark_results* diff --git a/tests/benchmark.py b/tests/benchmark.py index 762e4c5..a7a1597 100644 --- a/tests/benchmark.py +++ b/tests/benchmark.py @@ -1,61 +1,214 @@ import copapy as cp -import numpy as np import time +import json +import os +import subprocess +import sys +import numpy as np +from numpy.core._multiarray_umath import __cpu_features__ -def cp_vs_python(): +from copapy._matrices import diagonal - from numpy.core._multiarray_umath import __cpu_features__ - print(__cpu_features__) - - for v_size in range(10, 800, 40): - - sum_size = 10 - #v_size = 400 - iter_size = 30000 - - v1 = cp.vector(cp.variable(float(v)) for v in range(v_size)) - v2 = cp.vector(cp.variable(float(v)) for v in [5]*v_size) - - v3 = sum((v1 + i) @ v2 for i in range(sum_size)) - - tg = cp.Target() - tg.compile(v3) - - time.sleep(0.1) - t0 = time.perf_counter() - for _ in range(iter_size): - tg.run() - elapsed_cp = time.perf_counter() - t0 - - #print(f"Copapy: {elapsed_cp:.4f} s") +CPU_SIMD_FEATURES = "SSE SSE2 SSE3 SSSE3 SSE41 SSE42 AVX AVX2 AVX512F FMA3" - v1 = cp.vector(float(v) for v in range(v_size)) - v2 = cp.vector(float(v) for v in [5]*v_size) +def cp_vs_python(path: str): + os.environ.get("NPY_DISABLE_CPU_FEATURES") + cpu_f = CPU_SIMD_FEATURES.split(' ') + print('\n'.join(f"> {k}: {v}" for k, v in __cpu_features__.items() if k in cpu_f)) + + + results: list[dict[str, str | float | int]] = [] + + for _ in range(7): + for v_size in [10, 20, 40, 60] + list(range(100, 500, 50)): + + sum_size = 10 + #v_size = 400 + iter_size = 30000 + + v1 = cp.vector(cp.variable(float(v)) for v in range(v_size)) + v2 = cp.vector(cp.variable(float(v)) for v in [5]*v_size) - time.sleep(0.1) - t0 = time.perf_counter() - for _ in range(iter_size//10): v3 = sum((v1 + i) @ v2 for i in range(sum_size)) - elapsed_python = time.perf_counter() - t0 + tg = cp.Target() + tg.compile(v3) - #print(f"Python: {elapsed_python:.4f} s") + time.sleep(0.1) + t0 = time.perf_counter() + for _ in range(iter_size): + tg.run() + elapsed_cp = time.perf_counter() - t0 + + #print(f"Copapy: {elapsed_cp:.4f} s") + results.append({'benchmark': 'Copapy', 'iter_size': iter_size, 'elapsed_time': elapsed_cp, 'sum_size': sum_size, 'v_size': v_size}) - i = np.array(list(range(sum_size)),).reshape([sum_size, 1]) - time.sleep(0.1) - t0 = time.perf_counter() - for _ in range(iter_size): - v3 = np.sum((v1 + i) @ v2) + v1 = cp.vector(float(v) for v in range(v_size)) + v2 = cp.vector(float(v) for v in [5]*v_size) - elapsed_np2 = time.perf_counter() - t0 + time.sleep(0.1) + t0 = time.perf_counter() + for _ in range(iter_size//10): + v3 = sum((v1 + i) @ v2 for i in range(sum_size)) - #print(f"Numpy 2: {elapsed_np2:.4f} s") + elapsed_python = time.perf_counter() - t0 + + #print(f"Python: {elapsed_python:.4f} s") + results.append({'benchmark': 'Python','iter_size': iter_size//10, 'elapsed_time': elapsed_python, 'sum_size': sum_size, 'v_size': v_size}) + + v1 = np.array(list(range(v_size)), dtype=np.float32) + v2 = np.array([5]*v_size, dtype=np.float32) + i = np.array(list(range(sum_size)), dtype=np.int32).reshape([sum_size, 1]) + + time.sleep(0.1) + t0 = time.perf_counter() + for _ in range(iter_size): + v3 = np.sum((v1 + i) @ v2) + + elapsed_np = time.perf_counter() - t0 + + #print(f"Numpy 2: {elapsed_np2:.4f} s") + results.append({'benchmark': 'NumPy', 'iter_size': iter_size, 'elapsed_time': elapsed_np, 'sum_size': sum_size, 'v_size': v_size}) - print(f"{elapsed_cp}, {elapsed_python}, {elapsed_np2}") + print(f"{v_size} {elapsed_cp}, {elapsed_python}, {elapsed_np}") + + with open(path, 'w') as f: + json.dump(results, f) + + +def cp_vs_python_sparse(path: str = 'benchmark_results_001_sparse.json'): + results: list[dict[str, str | float | int]] = [] + + for _ in range(7): + for v_size in [10, 20, 40, 60] + list(range(100, 500, 50)): + + sum_size = 10 + #v_size = 400 + iter_size = 30000 + + v1 = cp.vector(cp.variable(float(v)) for v in range(v_size)) + v2 = cp.vector(cp.variable(float(v)) for v in [5]*v_size) + + v3 = sum((cp.diagonal(v1) + i) @ v2 for i in range(sum_size)) + + tg = cp.Target() + tg.compile(v3) + + time.sleep(0.1) + t0 = time.perf_counter() + for _ in range(iter_size): + tg.run() + elapsed_cp = time.perf_counter() - t0 + + #print(f"Copapy: {elapsed_cp:.4f} s") + results.append({'benchmark': 'Copapy', 'iter_size': iter_size, 'elapsed_time': elapsed_cp, 'sum_size': sum_size, 'v_size': v_size}) + + + + v1 = cp.vector(float(v) for v in range(v_size)) + v2 = cp.vector(float(v) for v in [5]*v_size) + + time.sleep(0.1) + t0 = time.perf_counter() + for _ in range(iter_size//10): + v3 = sum((cp.diagonal(v1) + i) @ v2 for i in range(sum_size)) + + elapsed_python = time.perf_counter() - t0 + + #print(f"Python: {elapsed_python:.4f} s") + results.append({'benchmark': 'Python','iter_size': iter_size//10, 'elapsed_time': elapsed_python, 'sum_size': sum_size, 'v_size': v_size}) + + v1 = np.array(list(range(v_size)), dtype=np.float32) + v2 = np.array([5]*v_size, dtype=np.float32) + i = np.array(list(range(sum_size)), dtype=np.int32).reshape([sum_size, 1, 1]) + + time.sleep(0.1) + t0 = time.perf_counter() + for _ in range(iter_size): + v3 = np.sum((v1 * np.eye(v_size) + i) @ v2) + + elapsed_np = time.perf_counter() - t0 + + #print(f"Numpy 2: {elapsed_np2:.4f} s") + results.append({'benchmark': 'NumPy', 'iter_size': iter_size, 'elapsed_time': elapsed_np, 'sum_size': sum_size, 'v_size': v_size}) + + + print(f"{v_size} {elapsed_cp}, {elapsed_python}, {elapsed_np}") + + with open(path, 'w') as f: + json.dump(results, f) + + +def plot_results(path: str): + import json + import matplotlib.pyplot as plt + import numpy as np + from collections import defaultdict + + # Load the benchmark results + with open(path, 'r') as f: + results = json.load(f) + + # Group data by benchmark and v_size, then calculate medians + data_by_benchmark = defaultdict(lambda: defaultdict(list)) + + for entry in results: + benchmark = entry['benchmark'] + v_size = entry['v_size'] + elapsed_time = entry['elapsed_time'] + data_by_benchmark[benchmark][v_size].append(elapsed_time) + + # Calculate medians + medians_by_benchmark = {} + for benchmark, v_sizes in data_by_benchmark.items(): + medians_by_benchmark[benchmark] = { + v_size: np.median(times) + for v_size, times in v_sizes.items() + } + + # Sort by v_size for plotting + benchmarks = sorted(medians_by_benchmark.keys()) + v_sizes_set = sorted(set(v for benchmark_data in medians_by_benchmark.values() for v in benchmark_data.keys())) + + # Create the plot + plt.figure(figsize=(10, 6)) + + for benchmark in benchmarks: + if benchmark != 'Python': + v_sizes = sorted(medians_by_benchmark[benchmark].keys()) + elapsed_times = [medians_by_benchmark[benchmark][v] for v in v_sizes] + plt.plot(v_sizes, elapsed_times, '.', label=benchmark) + + plt.xlabel('Vector Size (v_size)') + plt.ylabel('Elapsed Time (seconds)') + #plt.title('Benchmark Results: Elapsed Time vs Vector Size') + plt.legend() + #plt.grid(True, alpha=0.3) + plt.ylim(bottom=0) + plt.tight_layout() + + # Save to PNG + plt.savefig(path.replace('.json', '') + '.png', dpi=300) + print("Plot saved") + if __name__ == "__main__": - cp_vs_python() \ No newline at end of file + path1 = 'benchmark_results_001.json' + path2 = 'benchmark_results_001_sparse.json' + + if 'no_simd' in sys.argv[1:]: + os.environ["NPY_DISABLE_CPU_FEATURES"] = CPU_SIMD_FEATURES + subprocess.run([sys.executable, "tests/benchmark.py"]) + elif 'plot' in sys.argv[1:]: + plot_results(path1) + plot_results(path2) + else: + cp_vs_python(path1) + plot_results(path1) + + cp_vs_python_sparse(path2) + plot_results(path2) From 959d80b082fd0bef4ae5a5688503f67c0dcc29a7 Mon Sep 17 00:00:00 2001 From: Nicolas Kruse Date: Fri, 5 Dec 2025 08:28:02 +0100 Subject: [PATCH 24/39] shape property added and __getitem__ of matrix extended --- src/copapy/_matrices.py | 28 +++++++++++++++++++++++----- src/copapy/_vectors.py | 5 +++++ 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/src/copapy/_matrices.py b/src/copapy/_matrices.py index 3086a8c..906b983 100644 --- a/src/copapy/_matrices.py +++ b/src/copapy/_matrices.py @@ -34,13 +34,26 @@ class matrix(Generic[TNum]): return f"matrix({self.values})" def __len__(self) -> int: + """Return the number of rows in the matrix.""" return self.rows - def __getitem__(self, key: tuple[int, int]) -> variable[TNum] | TNum: - assert len(key) == 2 - row = key[0] - col = key[1] - return self.values[row][col] + @overload + def __getitem__(self, key: int) -> vector[TNum]: ... + @overload + def __getitem__(self, key: tuple[int, int]) -> variable[TNum] | TNum: ... + def __getitem__(self, key: int | tuple[int, int]) -> Any: + """Get a row as a vector or a specific element. + Args: + key: row index or (row, col) tuple + + Returns: + vector if row index is given, else the element at (row, col) + """ + if isinstance(key, tuple): + assert len(key) == 2 + return self.values[key[0]][key[1]] + else: + return vector(self.values[key]) def __iter__(self) -> Iterator[tuple[variable[TNum] | TNum, ...]]: return iter(self.values) @@ -203,6 +216,11 @@ class matrix(Generic[TNum]): tuple(self.values[i][j] for i in range(self.rows)) for j in range(self.cols) ) + + @property + def shape(self) -> tuple[int, int]: + """Return the shape of the matrix as (rows, cols).""" + return (self.rows, self.cols) @property def T(self) -> 'matrix[TNum]': diff --git a/src/copapy/_vectors.py b/src/copapy/_vectors.py index 4188d6d..3c50d2a 100644 --- a/src/copapy/_vectors.py +++ b/src/copapy/_vectors.py @@ -193,6 +193,11 @@ class vector(Generic[TNum]): 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) + + @property + def shape(self) -> tuple[int]: + """Return the shape of the vector as (length,).""" + return (len(self.values),) @overload def sum(self: 'vector[int]') -> int | variable[int]: ... From d526c5ddc0c58f9cd4b6c1387ab12ae7f41bff30 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Sat, 6 Dec 2025 15:11:42 +0100 Subject: [PATCH 25/39] added get_all_dag_edges_between function --- src/copapy/_compiler.py | 49 ++++++++++++++++++++++++++++++++++++----- src/copapy/backend.py | 3 ++- 2 files changed, 46 insertions(+), 6 deletions(-) diff --git a/src/copapy/_compiler.py b/src/copapy/_compiler.py index a385874..61075ff 100644 --- a/src/copapy/_compiler.py +++ b/src/copapy/_compiler.py @@ -55,6 +55,43 @@ def stable_toposort(edges: Iterable[tuple[Node, Node]]) -> list[Node]: return result +def get_all_dag_edges_between(roots: Iterable[Node], leaves: Iterable[Node]) -> Generator[tuple[Node, Node], None, None]: + """Get all edges in the DAG connecting given roots with given leaves + + Arguments: + nodes: Iterable of nodes to start the traversal from + + Yields: + Tuples of (source_node, target_node) representing edges in the DAG + """ + # Walk the full DAG starting from given roots to final leaves + parent_lookup: dict[Node, set[Node]] = dict() + node_list: list[Node] = [n for n in roots] + while(node_list): + node = node_list.pop() + for net in node.args: + if net.source in parent_lookup: + parent_lookup[net.source].add(node) + else: + parent_lookup[net.source] = {node} + node_list.append(net.source) + + # Walk the DAG in reverse direction starting from given leaves to given roots + emitted_edges: set[tuple[Node, Node]] = set() + node_list = [n for n in leaves] + while(node_list): + child_node = node_list.pop() + if child_node in parent_lookup: + for node in parent_lookup[child_node]: + edge = (child_node, node) + if edge not in emitted_edges: + yield edge + node_list.append(node) + emitted_edges.add(edge) + + assert all(r in {e[0] for e in emitted_edges} for r in leaves) + + def get_all_dag_edges(nodes: Iterable[Node]) -> Generator[tuple[Node, Node], None, None]: """Get all edges in the DAG by traversing from the given nodes @@ -64,15 +101,17 @@ def get_all_dag_edges(nodes: Iterable[Node]) -> Generator[tuple[Node, Node], Non Yields: Tuples of (source_node, target_node) representing edges in the DAG """ - emitted_nodes: set[tuple[Node, Node]] = set() + emitted_edges: set[tuple[Node, Node]] = set() + node_list: list[Node] = [n for n in nodes] - for node in nodes: - yield from get_all_dag_edges(net.source for net in node.args) + while(node_list): + node = node_list.pop() for net in node.args: edge = (net.source, node) - if edge not in emitted_nodes: + if edge not in emitted_edges: yield edge - emitted_nodes.add(edge) + node_list.append(net.source) + emitted_edges.add(edge) def get_const_nets(nodes: list[Node]) -> list[Net]: diff --git a/src/copapy/backend.py b/src/copapy/backend.py index 839bef9..c03c76c 100644 --- a/src/copapy/backend.py +++ b/src/copapy/backend.py @@ -1,7 +1,7 @@ from ._target import add_read_command from ._basic_types import Net, Op, Node, CPConstant, Write, stencil_db_from_package from ._compiler import compile_to_dag, \ - stable_toposort, get_const_nets, get_all_dag_edges, add_read_ops, \ + stable_toposort, get_const_nets, get_all_dag_edges, add_read_ops, get_all_dag_edges_between, \ add_write_ops __all__ = [ @@ -15,6 +15,7 @@ __all__ = [ "stable_toposort", "get_const_nets", "get_all_dag_edges", + "get_all_dag_edges_between", "add_read_ops", "add_write_ops", "stencil_db_from_package" From a21970de79eeb19ea122cd8cc80e5d849804a8f3 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Sat, 6 Dec 2025 15:13:28 +0100 Subject: [PATCH 26/39] type hint fixes --- src/copapy/_matrices.py | 5 +++-- src/copapy/_target.py | 8 ++++---- src/copapy/_vectors.py | 31 ++++++++++++++++++++++++++++++- 3 files changed, 37 insertions(+), 7 deletions(-) diff --git a/src/copapy/_matrices.py b/src/copapy/_matrices.py index 906b983..773ac38 100644 --- a/src/copapy/_matrices.py +++ b/src/copapy/_matrices.py @@ -20,9 +20,10 @@ class matrix(Generic[TNum]): values: iterable of iterable of constant values and variables """ if isinstance(values, vector): - rows = (values.values,) + rows = [values.values] else: - rows = tuple(tuple(row) for row in values) + rows = [tuple(row) for row in values] + if rows: row_len = len(rows[0]) assert all(len(row) == row_len for row in rows), "All rows must have the same length" diff --git a/src/copapy/_target.py b/src/copapy/_target.py index 13fe4f6..d2bbed4 100644 --- a/src/copapy/_target.py +++ b/src/copapy/_target.py @@ -40,11 +40,11 @@ class Target(): for s in variables: if isinstance(s, Iterable): for net in s: - assert isinstance(net, Net), f"The folowing element is not a Net: {net}" - nodes.append(Write(net)) + if isinstance(net, Net): + nodes.append(Write(net)) else: - assert isinstance(s, Net), f"The folowing element is not a Net: {s}" - nodes.append(Write(s)) + if isinstance(s, Net): + nodes.append(Write(s)) dw, self._variables = compile_to_dag(nodes, self.sdb) dw.write_com(binw.Command.END_COM) diff --git a/src/copapy/_vectors.py b/src/copapy/_vectors.py index 3c50d2a..44d5869 100644 --- a/src/copapy/_vectors.py +++ b/src/copapy/_vectors.py @@ -30,7 +30,13 @@ class vector(Generic[TNum]): def __len__(self) -> int: return len(self.values) - def __getitem__(self, index: int) -> variable[TNum] | TNum: + @overload + def __getitem__(self, index: int) -> variable[TNum] | TNum: ... + @overload + def __getitem__(self, index: slice) -> 'vector[TNum]': ... + def __getitem__(self, index: int | slice) -> 'vector[TNum] | variable[TNum] | TNum': + if isinstance(index, slice): + return vector(self.values[index]) return self.values[index] def __neg__(self) -> 'vector[TNum]': @@ -111,6 +117,29 @@ class vector(Generic[TNum]): def __rmul__(self, other: VecNumLike) -> Any: return self * other + @overload + def __pow__(self: 'vector[int]', other: VecFloatLike) -> 'vector[float]': ... + @overload + def __pow__(self: 'vector[int]', other: VecIntLike) -> 'vector[int]': ... + @overload + def __pow__(self: 'vector[float]', other: VecNumLike) -> 'vector[float]': ... + @overload + def __pow__(self, other: VecNumLike) -> 'vector[int] | vector[float]': ... + def __pow__(self, other: VecNumLike) -> Any: + 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) + + @overload + def __rpow__(self: 'vector[float]', other: VecNumLike) -> 'vector[float]': ... + @overload + def __rpow__(self: 'vector[int]', other: variable[int] | int) -> 'vector[int]': ... + @overload + def __rpow__(self, other: VecNumLike) -> 'vector[Any]': ... + def __rpow__(self, other: VecNumLike) -> Any: + return self ** other + def __truediv__(self, other: VecNumLike) -> 'vector[float]': if isinstance(other, vector): assert len(self.values) == len(other.values) From 19fc403d37cb85c7fcc46b98fc97a4073989d283 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Sat, 6 Dec 2025 15:14:06 +0100 Subject: [PATCH 27/39] autograd updated with get_all_dag_edges_between to increase speed --- src/copapy/_autograd.py | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/src/copapy/_autograd.py b/src/copapy/_autograd.py index 685d9e4..ea5e9c4 100644 --- a/src/copapy/_autograd.py +++ b/src/copapy/_autograd.py @@ -6,14 +6,14 @@ from ._basic_types import Net, unifloat @overload -def grad(x: variable[Any], y: variable[Any]) -> unifloat: ... +def grad(x: Any, y: variable[Any]) -> unifloat: ... @overload -def grad(x: variable[Any], y: Sequence[variable[Any]]) -> list[unifloat]: ... +def grad(x: Any, y: vector[Any]) -> vector[float]: ... @overload -def grad(x: variable[Any], y: vector[Any]) -> vector[float]: ... +def grad(x: Any, y: Sequence[variable[Any]]) -> list[unifloat]: ... @overload -def grad(x: variable[Any], y: matrix[Any]) -> matrix[float]: ... -def grad(x: variable[Any], y: variable[Any] | Sequence[variable[Any]] | vector[Any] | matrix[float]) -> Any: +def grad(x: Any, y: matrix[Any]) -> matrix[float]: ... +def grad(x: Any, y: variable[Any] | Sequence[variable[Any]] | vector[Any] | matrix[Any]) -> Any: """Returns the partial derivative dx/dy where x needs to be a scalar and y might be a scalar, a list of scalars, a vector or matrix. @@ -24,7 +24,17 @@ def grad(x: variable[Any], y: variable[Any] | Sequence[variable[Any]] | vector[A Returns: Derivative of x with the type and dimensions of y """ - edges = cpb.get_all_dag_edges([x.source]) + assert isinstance(x, variable), f"Argument x for grad function must be a variable but is {type(x)}." + + if isinstance(y, variable): + y_set = {y} + if isinstance(y, matrix): + y_set = {v for row in y for v in row} + else: + assert isinstance(y, Sequence) or isinstance(y, vector) + y_set = {v for v in y} + + edges = cpb.get_all_dag_edges_between([x.source], (net.source for net in y_set if isinstance(net, Net))) ordered_ops = cpb.stable_toposort(edges) net_lookup = {net.source: net for node in ordered_ops for net in node.args} @@ -34,7 +44,7 @@ def grad(x: variable[Any], y: variable[Any] | Sequence[variable[Any]] | vector[A grad_dict[val] = grad_dict.get(val, 0.0) + gradient_value for node in reversed(ordered_ops): - print(f"--> {'x' if node in net_lookup else ' '}", node, f"{net_lookup.get(node)}") + #print(f"--> {'x' if node in net_lookup else ' '}", node, f"{net_lookup.get(node)}") if node.args: args: Sequence[Any] = list(node.args) g = 1.0 if node is x.source else grad_dict[net_lookup[node]] From d772d9fa2ec02a387f9e896183e48be6c2e9ebad Mon Sep 17 00:00:00 2001 From: Nicolas Date: Sat, 6 Dec 2025 15:14:31 +0100 Subject: [PATCH 28/39] inverse kinematic test added --- tests/test_rev_kinematics.py | 48 ++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 tests/test_rev_kinematics.py diff --git a/tests/test_rev_kinematics.py b/tests/test_rev_kinematics.py new file mode 100644 index 0000000..6d67f59 --- /dev/null +++ b/tests/test_rev_kinematics.py @@ -0,0 +1,48 @@ +import numpy as np +import copapy as cp + +# Arm lengths +l1, l2 = 1.8, 2.0 + +# Target position +target = np.array([0.7, 0.7]) + +# Learning rate for iterative adjustment +alpha = 0.1 + + +def forward_kinematics(theta1: cp.variable[float] | float, theta2: cp.variable[float] | float) -> tuple[cp.vector[float], cp.vector[float]]: + """Return positions of joint and end-effector.""" + joint = cp.vector([l1 * cp.cos(theta1), l1 * cp.sin(theta1)]) + end_effector = joint + cp.vector([l2 * cp.cos(theta1 + theta2), + l2 * cp.sin(theta1 + theta2)]) + return joint, end_effector + + +def test_two_arms(): + target_vec = cp.vector(target) + theta = cp.vector([cp.variable(0.0), cp.variable(0.0)]) + + joint = cp.vector([0.0, 0.0]) + effector = cp.vector([0.0, 0.0]) + error = 0.0 + + # Iterative IK + for _ in range(48): + joint, effector = forward_kinematics(theta[0], theta[1]) + error = ((target_vec - effector) ** 2).sum() + + grad_vec = cp.grad(error, theta) + theta -= alpha * grad_vec + + tg = cp.Target() + tg.compile(error, theta, joint) + tg.run() + + print(f"Joint angles: {tg.read_value(theta)}") + print(f"Joint position: {tg.read_value(joint)}") + print(f"End-effector position: {tg.read_value(effector)} quadratic error = {tg.read_value(error)}") + + +if __name__ == '__main__': + test_two_arms() \ No newline at end of file From 42116b96c9d935f38c8cbbd19c80ecd222e0743b Mon Sep 17 00:00:00 2001 From: Nicolas Date: Sat, 6 Dec 2025 15:15:07 +0100 Subject: [PATCH 29/39] bench mark script updated --- tests/benchmark.py | 36 +++++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/tests/benchmark.py b/tests/benchmark.py index a7a1597..b2d7f7b 100644 --- a/tests/benchmark.py +++ b/tests/benchmark.py @@ -20,8 +20,8 @@ def cp_vs_python(path: str): results: list[dict[str, str | float | int]] = [] - for _ in range(7): - for v_size in [10, 20, 40, 60] + list(range(100, 500, 50)): + for _ in range(15): + for v_size in [10, 30, 60] + list(range(100, 600, 100)): sum_size = 10 #v_size = 400 @@ -51,7 +51,7 @@ def cp_vs_python(path: str): time.sleep(0.1) t0 = time.perf_counter() - for _ in range(iter_size//10): + for _ in range(iter_size//100): v3 = sum((v1 + i) @ v2 for i in range(sum_size)) elapsed_python = time.perf_counter() - t0 @@ -84,16 +84,25 @@ def cp_vs_python_sparse(path: str = 'benchmark_results_001_sparse.json'): results: list[dict[str, str | float | int]] = [] for _ in range(7): - for v_size in [10, 20, 40, 60] + list(range(100, 500, 50)): + for v_size in [8, 8, 16, 20, 24, 32]: + + n_ones = int((v_size ** 2) * 0.5) + n_zeros = (v_size ** 2) - n_ones + mask = np.array([1] * n_ones + [0] * n_zeros).reshape((v_size, v_size)) + np.random.shuffle(mask) sum_size = 10 #v_size = 400 - iter_size = 30000 + iter_size = 3000 v1 = cp.vector(cp.variable(float(v)) for v in range(v_size)) v2 = cp.vector(cp.variable(float(v)) for v in [5]*v_size) - v3 = sum((cp.diagonal(v1) + i) @ v2 for i in range(sum_size)) + test = cp.vector(np.linspace(0, 1, v_size)) + + assert False, test * v2 + + v3 = sum(((cp.diagonal(v1) + i) * cp.matrix(mask)) @ v2 for i in range(sum_size)) tg = cp.Target() tg.compile(v3) @@ -114,8 +123,8 @@ def cp_vs_python_sparse(path: str = 'benchmark_results_001_sparse.json'): time.sleep(0.1) t0 = time.perf_counter() - for _ in range(iter_size//10): - v3 = sum((cp.diagonal(v1) + i) @ v2 for i in range(sum_size)) + for _ in range(iter_size//1000): + v3 = sum(((cp.diagonal(v1) + i) * cp.matrix(mask)) @ v2 for i in range(sum_size)) elapsed_python = time.perf_counter() - t0 @@ -124,12 +133,13 @@ def cp_vs_python_sparse(path: str = 'benchmark_results_001_sparse.json'): v1 = np.array(list(range(v_size)), dtype=np.float32) v2 = np.array([5]*v_size, dtype=np.float32) - i = np.array(list(range(sum_size)), dtype=np.int32).reshape([sum_size, 1, 1]) + i_arr = np.array(list(range(sum_size)), dtype=np.int32).reshape([sum_size, 1, 1]) + tmp1 = v1 * np.eye(v_size) + i_arr time.sleep(0.1) t0 = time.perf_counter() for _ in range(iter_size): - v3 = np.sum((v1 * np.eye(v_size) + i) @ v2) + v3 = np.sum(((tmp1) * mask) @ v2) elapsed_np = time.perf_counter() - t0 @@ -205,10 +215,10 @@ if __name__ == "__main__": subprocess.run([sys.executable, "tests/benchmark.py"]) elif 'plot' in sys.argv[1:]: plot_results(path1) - plot_results(path2) + #plot_results(path2) else: cp_vs_python(path1) plot_results(path1) - cp_vs_python_sparse(path2) - plot_results(path2) + #cp_vs_python_sparse(path2) + #plot_results(path2) From 2b2f854ee4c56b87c7eaf12da8f1f01bb458881f Mon Sep 17 00:00:00 2001 From: Nicolas Date: Sat, 6 Dec 2025 15:15:33 +0100 Subject: [PATCH 30/39] windows build cmd script updated --- tools/build.bat | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tools/build.bat b/tools/build.bat index 5402235..b98b23e 100644 --- a/tools/build.bat +++ b/tools/build.bat @@ -46,8 +46,8 @@ wsl aarch64-linux-gnu-gcc-12 -static -Wall -Wextra -Wconversion -Wsign-conversio echo --------------arm-v6 32 bit---------------- REM sh ../copapy/tools/cross_compiler_unix/packobjs.sh arm-none-eabi-gcc arm-none-eabi-ld ../copapy/build/musl/musl_objects_armv6.o "-march=armv6 -mfpu=vfp -marm" -wsl arm-none-eabi-gcc -fno-pic -ffunction-sections -march=armv6 -mfpu=vfp -marm -c build/stencils/stencils.c -O3 -o build/stencils/stencils.o -wsl arm-none-eabi-ld -r build/stencils/stencils.o build/musl/musl_objects_armv6.o -o src/copapy/obj/stencils_armv6_O3.o +wsl arm-none-eabi-gcc -fno-pic -ffunction-sections -march=armv6 -mfpu=vfp -mfloat-abi=hard -marm -c build/stencils/stencils.c -O3 -o build/stencils/stencils.o +wsl arm-none-eabi-ld -r build/stencils/stencils.o build/musl/musl_objects_armv6.o $(arm-none-eabi-gcc -print-libgcc-file-name) -o src/copapy/obj/stencils_armv6_O3.o wsl arm-none-eabi-objdump -d -x src/copapy/obj/stencils_armv6_O3.o > build/stencils/stencils_armv6_O3.asm echo ------------------------------ REM echo - Build runner @@ -57,9 +57,11 @@ REM wsl arm-linux-gnueabihf-gcc -march=armv6 -mfpu=vfp -marm -static -Wall -Wext echo --------------arm-v7 32 bit---------------- REM sh ../copapy/tools/cross_compiler_unix/packobjs.sh arm-none-eabi-gcc arm-none-eabi-ld ../copapy/build/musl/musl_objects_armv7.o "-march=armv7-a -mfpu=neon-vfpv3 -marm" -wsl arm-none-eabi-gcc -fno-pic -ffunction-sections -march=armv7-a -mfpu=neon-vfpv3 -marm -c build/stencils/stencils.c -O3 -o build/stencils/stencils.o -wsl arm-none-eabi-ld -r build/stencils/stencils.o build/musl/musl_objects_armv7.o -o src/copapy/obj/stencils_armv7_O3.o +wsl arm-none-eabi-gcc -fno-pic -ffunction-sections -march=armv7-a -mfpu=neon-vfpv3 -mfloat-abi=hard -marm -c build/stencils/stencils.c -O3 -o build/stencils/stencils.o +wsl arm-none-eabi-ld -r build/stencils/stencils.o build/musl/musl_objects_armv7.o $(arm-none-eabi-gcc -print-libgcc-file-name) -o src/copapy/obj/stencils_armv7_O3.o wsl arm-none-eabi-objdump -d -x src/copapy/obj/stencils_armv7_O3.o > build/stencils/stencils_armv7_O3.asm + + echo ------------------------------ echo - Build runner wsl arm-linux-gnueabihf-gcc -march=armv7-a -mfpu=neon-vfpv3 -marm -static -Wall -Wextra -Wconversion -Wsign-conversion -Wshadow -Wstrict-overflow -O3 -DENABLE_LOGGING src/coparun/runmem.c src/coparun/coparun.c src/coparun/mem_man.c -o build/runner/coparun-armv7 From 1c230f365f640a30256e06483d7029daab47b52a Mon Sep 17 00:00:00 2001 From: Nicolas Date: Sat, 6 Dec 2025 15:55:22 +0100 Subject: [PATCH 31/39] numpy dependency removed for rev kin test --- tests/test_rev_kinematics.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_rev_kinematics.py b/tests/test_rev_kinematics.py index 6d67f59..abf04ab 100644 --- a/tests/test_rev_kinematics.py +++ b/tests/test_rev_kinematics.py @@ -1,11 +1,10 @@ -import numpy as np import copapy as cp # Arm lengths l1, l2 = 1.8, 2.0 # Target position -target = np.array([0.7, 0.7]) +target = cp.vector([0.7, 0.7]) # Learning rate for iterative adjustment alpha = 0.1 @@ -41,7 +40,8 @@ def test_two_arms(): print(f"Joint angles: {tg.read_value(theta)}") print(f"Joint position: {tg.read_value(joint)}") - print(f"End-effector position: {tg.read_value(effector)} quadratic error = {tg.read_value(error)}") + print(f"End-effector position: {tg.read_value(effector)}") + print(f"quadratic error = {tg.read_value(error)}") if __name__ == '__main__': From 423ca30ac633dfd064eac22ad61461b119fdec60 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Sat, 6 Dec 2025 15:55:38 +0100 Subject: [PATCH 32/39] readme updated with rev kin example --- README.md | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/README.md b/README.md index 86e914b..2cdb1e0 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,56 @@ print("Result d:", tg.read_value(d)) print("Result e:", tg.read_value(e)) ``` +An other example using autograd in copapy. Here for for implementing +gradient descent to solve a reverse kinematic problem for +a two joint 2D arm: + +```python +import copapy as cp + +# Arm lengths +l1, l2 = 1.8, 2.0 + +# Target position +target = cp.vector([0.7, 0.7]) + +# Learning rate for iterative adjustment +alpha = 0.1 + +def forward_kinematics(theta1, theta2): + """Return positions of joint and end-effector.""" + joint = cp.vector([l1 * cp.cos(theta1), l1 * cp.sin(theta1)]) + end_effector = joint + cp.vector([l2 * cp.cos(theta1 + theta2), + l2 * cp.sin(theta1 + theta2)]) + return joint, end_effector + +# Start values +theta = cp.vector([cp.variable(0.0), cp.variable(0.0)]) + +# Iterative inverse kinematic +for _ in range(48): + joint, effector = forward_kinematics(theta[0], theta[1]) + error = ((target - effector) ** 2).sum() + + grad_vec = cp.grad(error, theta) + theta -= alpha * grad_vec + +tg = cp.Target() +tg.compile(error, theta, joint) +tg.run() + +print(f"Joint angles: {tg.read_value(theta)}") +print(f"Joint position: {tg.read_value(joint)}") +print(f"End-effector position: {tg.read_value(effector)}") +print(f"quadratic error = {tg.read_value(error)}") +``` +``` +Joint angles: [-0.7221821546554565, 2.6245293617248535] +Joint position: [1.3509329557418823, -1.189529299736023] +End-effector position: [0.6995794177055359, 0.7014330625534058] +quadratic error = 2.2305819129542215e-06 +``` + ## How it works The **Compilation** step starts with tracing the python code to generate an acyclic directed graph (DAG) of variables and operations. The DAG can be optimized and gets than linearized to a sequence of operations. Each operation gets mapped to a pre-compiled stencil, which is a piece of machine code with placeholders for memory addresses. The compiler generates patch instructions to fill the placeholders with the correct memory addresses. The binary code build from the stencils, data for constants and the patch instructions are than passed to the runner for execution. The runner allocates memory for the code and data, applies the patch instructions to correct memory addresses and finally executes the code. From 2e48fe5ec289064a95f89b637025d98f78911530 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Sat, 6 Dec 2025 18:09:25 +0100 Subject: [PATCH 33/39] "variable" type renamed to "value" --- src/copapy/__init__.py | 4 +- src/copapy/_autograd.py | 24 ++-- src/copapy/_basic_types.py | 198 +++++++++++++++---------------- src/copapy/_math.py | 104 ++++++++-------- src/copapy/_matrices.py | 52 ++++---- src/copapy/_mixed.py | 22 ++-- src/copapy/_target.py | 58 ++++----- src/copapy/_vectors.py | 70 +++++------ src/copapy/filters.py | 12 +- tests/benchmark.py | 8 +- tests/test_ast_gen.py | 6 +- tests/test_autograd.py | 6 +- tests/test_branching_stencils.py | 4 +- tests/test_comp_timing.py | 4 +- tests/test_compile.py | 4 +- tests/test_compile_aarch64.py | 4 +- tests/test_compile_armv7.py | 4 +- tests/test_compile_div.py | 4 +- tests/test_compile_math.py | 8 +- tests/test_coparun_module.py | 4 +- tests/test_math.py | 34 +++--- tests/test_matrix.py | 14 +-- tests/test_ops.py | 12 +- tests/test_ops_aarch64.py | 12 +- tests/test_ops_armv7.py | 12 +- tests/test_ops_x86.py | 8 +- tests/test_prog_flow.py | 8 +- tests/test_readme_example.py | 4 +- tests/test_rev_kinematics.py | 4 +- tests/test_vector.py | 20 ++-- tools/make_example.py | 10 +- 31 files changed, 369 insertions(+), 369 deletions(-) diff --git a/src/copapy/__init__.py b/src/copapy/__init__.py index 1309d5f..6e6ea03 100644 --- a/src/copapy/__init__.py +++ b/src/copapy/__init__.py @@ -1,5 +1,5 @@ from ._target import Target -from ._basic_types import NumLike, variable, generic_sdb, iif +from ._basic_types import NumLike, value, generic_sdb, iif from ._vectors import vector, distance, scalar_projection, angle_between, rotate_vector, vector_projection from ._matrices import matrix, identity, zeros, ones, diagonal, eye from ._math import sqrt, abs, sign, sin, cos, tan, asin, acos, atan, atan2, log, exp, pow, get_42, clamp, min, max, relu @@ -8,7 +8,7 @@ from ._autograd import grad __all__ = [ "Target", "NumLike", - "variable", + "value", "generic_sdb", "iif", "vector", diff --git a/src/copapy/_autograd.py b/src/copapy/_autograd.py index ea5e9c4..b09da2d 100644 --- a/src/copapy/_autograd.py +++ b/src/copapy/_autograd.py @@ -1,4 +1,4 @@ -from . import variable, vector, matrix +from . import value, vector, matrix import copapy.backend as cpb from typing import Any, Sequence, overload import copapy as cp @@ -6,14 +6,14 @@ from ._basic_types import Net, unifloat @overload -def grad(x: Any, y: variable[Any]) -> unifloat: ... +def grad(x: Any, y: value[Any]) -> unifloat: ... @overload def grad(x: Any, y: vector[Any]) -> vector[float]: ... @overload -def grad(x: Any, y: Sequence[variable[Any]]) -> list[unifloat]: ... +def grad(x: Any, y: Sequence[value[Any]]) -> list[unifloat]: ... @overload def grad(x: Any, y: matrix[Any]) -> matrix[float]: ... -def grad(x: Any, y: variable[Any] | Sequence[variable[Any]] | vector[Any] | matrix[Any]) -> Any: +def grad(x: Any, y: value[Any] | Sequence[value[Any]] | vector[Any] | matrix[Any]) -> Any: """Returns the partial derivative dx/dy where x needs to be a scalar and y might be a scalar, a list of scalars, a vector or matrix. @@ -24,9 +24,9 @@ def grad(x: Any, y: variable[Any] | Sequence[variable[Any]] | vector[Any] | matr Returns: Derivative of x with the type and dimensions of y """ - assert isinstance(x, variable), f"Argument x for grad function must be a variable but is {type(x)}." + assert isinstance(x, value), f"Argument x for grad function must be a copapy value but is {type(x)}." - if isinstance(y, variable): + if isinstance(y, value): y_set = {y} if isinstance(y, matrix): y_set = {v for row in y for v in row} @@ -40,7 +40,7 @@ def grad(x: Any, y: variable[Any] | Sequence[variable[Any]] | vector[Any] | matr net_lookup = {net.source: net for node in ordered_ops for net in node.args} grad_dict: dict[Net, unifloat] = dict() - def add_grad(val: variable[Any], gradient_value: unifloat) -> None: + def add_grad(val: value[Any], gradient_value: unifloat) -> None: grad_dict[val] = grad_dict.get(val, 0.0) + gradient_value for node in reversed(ordered_ops): @@ -49,8 +49,8 @@ def grad(x: Any, y: variable[Any] | Sequence[variable[Any]] | vector[Any] | matr args: Sequence[Any] = list(node.args) g = 1.0 if node is x.source else grad_dict[net_lookup[node]] opn = node.name.split('_')[0] - a: variable[Any] = args[0] - b: variable[Any] = args[1] if len(args) > 1 else a + a: value[Any] = args[0] + b: value[Any] = args[1] if len(args) > 1 else a if opn in ['ge', 'gt', 'eq', 'ne', 'floordiv', 'bwand', 'bwor', 'bwxor']: pass # Derivative is 0 for all ops returning integers @@ -117,10 +117,10 @@ def grad(x: Any, y: variable[Any] | Sequence[variable[Any]] | vector[Any] | matr else: raise ValueError(f"Operation {opn} not yet supported for auto diff.") - if isinstance(y, variable): + if isinstance(y, value): return grad_dict[y] if isinstance(y, vector): - return vector(grad_dict[yi] if isinstance(yi, variable) else 0.0 for yi in y) + return vector(grad_dict[yi] if isinstance(yi, value) else 0.0 for yi in y) if isinstance(y, matrix): - return matrix((grad_dict[yi] if isinstance(yi, variable) else 0.0 for yi in row) for row in y) + return matrix((grad_dict[yi] if isinstance(yi, value) else 0.0 for yi in row) for row in y) return [grad_dict[yi] for yi in y] diff --git a/src/copapy/_basic_types.py b/src/copapy/_basic_types.py index 879488a..aa399dd 100644 --- a/src/copapy/_basic_types.py +++ b/src/copapy/_basic_types.py @@ -4,12 +4,12 @@ from ._stencils import stencil_database, detect_process_arch import copapy as cp from ._helper_types import TNum -NumLike: TypeAlias = 'variable[int] | variable[float] | int | float' -unifloat: TypeAlias = 'variable[float] | float' -uniint: TypeAlias = 'variable[int] | int' +NumLike: TypeAlias = 'value[int] | value[float] | int | float' +unifloat: TypeAlias = 'value[float] | float' +uniint: TypeAlias = 'value[int] | int' -TCPNum = TypeVar("TCPNum", bound='variable[Any]') -TVarNumb: TypeAlias = 'variable[Any] | int | float' +TCPNum = TypeVar("TCPNum", bound='value[Any]') +TVarNumb: TypeAlias = 'value[Any] | int | float' stencil_cache: dict[tuple[str, str], stencil_database] = {} @@ -67,7 +67,7 @@ class Node: class Net: - """A Net represents a variable in the computation graph - or more generally it + """A Net represents a scalar type in the computation graph - or more generally it connects Nodes together. Attributes: @@ -86,19 +86,19 @@ class Net: return self.source.node_hash -class variable(Generic[TNum], Net): - """A "variable" represents a typed variable. It supports arithmetic and +class value(Generic[TNum], Net): + """A "value" represents a typed scalar variable. It supports arithmetic and comparison operations. Attributes: - dtype (str): Data type of this variable. + dtype (str): Data type of this value. """ def __init__(self, source: TNum | Node, dtype: str | None = None): - """Instance a variable. + """Instance a value. Args: source: A numeric value or Node object. - dtype: Data type of this variable. Required if source is a Node. + dtype: Data type of this value. Required if source is a Node. """ if isinstance(source, Node): self.source = source @@ -115,67 +115,67 @@ class variable(Generic[TNum], Net): self.dtype = 'int' @overload - def __add__(self: 'variable[TNum]', other: 'variable[TNum] | TNum') -> 'variable[TNum]': ... + def __add__(self: 'value[TNum]', other: 'value[TNum] | TNum') -> 'value[TNum]': ... @overload - def __add__(self: 'variable[int]', other: uniint) -> 'variable[int]': ... + def __add__(self: 'value[int]', other: uniint) -> 'value[int]': ... @overload - def __add__(self, other: unifloat) -> 'variable[float]': ... + def __add__(self, other: unifloat) -> 'value[float]': ... @overload - def __add__(self: 'variable[float]', other: NumLike) -> 'variable[float]': ... + def __add__(self: 'value[float]', other: NumLike) -> 'value[float]': ... @overload - def __add__(self, other: TVarNumb) -> 'variable[float] | variable[int]': ... + def __add__(self, other: TVarNumb) -> 'value[float] | value[int]': ... def __add__(self, other: TVarNumb) -> Any: - if not isinstance(other, variable) and other == 0: + if not isinstance(other, value) and other == 0: return self return add_op('add', [self, other], True) @overload - def __radd__(self: 'variable[TNum]', other: TNum) -> 'variable[TNum]': ... + def __radd__(self: 'value[TNum]', other: TNum) -> 'value[TNum]': ... @overload - def __radd__(self: 'variable[int]', other: int) -> 'variable[int]': ... + def __radd__(self: 'value[int]', other: int) -> 'value[int]': ... @overload - def __radd__(self, other: float) -> 'variable[float]': ... + def __radd__(self, other: float) -> 'value[float]': ... def __radd__(self, other: NumLike) -> Any: return self + other @overload - def __sub__(self: 'variable[TNum]', other: 'variable[TNum] | TNum') -> 'variable[TNum]': ... + def __sub__(self: 'value[TNum]', other: 'value[TNum] | TNum') -> 'value[TNum]': ... @overload - def __sub__(self: 'variable[int]', other: uniint) -> 'variable[int]': ... + def __sub__(self: 'value[int]', other: uniint) -> 'value[int]': ... @overload - def __sub__(self, other: unifloat) -> 'variable[float]': ... + def __sub__(self, other: unifloat) -> 'value[float]': ... @overload - def __sub__(self: 'variable[float]', other: NumLike) -> 'variable[float]': ... + def __sub__(self: 'value[float]', other: NumLike) -> 'value[float]': ... @overload - def __sub__(self, other: TVarNumb) -> 'variable[float] | variable[int]': ... + def __sub__(self, other: TVarNumb) -> 'value[float] | value[int]': ... def __sub__(self, other: TVarNumb) -> Any: if isinstance(other, int | float) and other == 0: return self return add_op('sub', [self, other]) @overload - def __rsub__(self: 'variable[TNum]', other: TNum) -> 'variable[TNum]': ... + def __rsub__(self: 'value[TNum]', other: TNum) -> 'value[TNum]': ... @overload - def __rsub__(self: 'variable[int]', other: int) -> 'variable[int]': ... + def __rsub__(self: 'value[int]', other: int) -> 'value[int]': ... @overload - def __rsub__(self, other: float) -> 'variable[float]': ... + def __rsub__(self, other: float) -> 'value[float]': ... def __rsub__(self, other: NumLike) -> Any: return add_op('sub', [other, self]) @overload - def __mul__(self: 'variable[TNum]', other: 'variable[TNum] | TNum') -> 'variable[TNum]': ... + def __mul__(self: 'value[TNum]', other: 'value[TNum] | TNum') -> 'value[TNum]': ... @overload - def __mul__(self: 'variable[int]', other: uniint) -> 'variable[int]': ... + def __mul__(self: 'value[int]', other: uniint) -> 'value[int]': ... @overload - def __mul__(self, other: unifloat) -> 'variable[float]': ... + def __mul__(self, other: unifloat) -> 'value[float]': ... @overload - def __mul__(self: 'variable[float]', other: NumLike) -> 'variable[float]': ... + def __mul__(self: 'value[float]', other: NumLike) -> 'value[float]': ... @overload - def __mul__(self, other: TVarNumb) -> 'variable[float] | variable[int]': ... + def __mul__(self, other: TVarNumb) -> 'value[float] | value[int]': ... def __mul__(self, other: TVarNumb) -> Any: if self.dtype == 'float' and isinstance(other, int): other = float(other) # Prevent runtime conversion of consts; TODO: add this for other operations - if not isinstance(other, variable): + if not isinstance(other, value): if other == 1: return self elif other == 0: @@ -183,112 +183,112 @@ class variable(Generic[TNum], Net): return add_op('mul', [self, other], True) @overload - def __rmul__(self: 'variable[TNum]', other: TNum) -> 'variable[TNum]': ... + def __rmul__(self: 'value[TNum]', other: TNum) -> 'value[TNum]': ... @overload - def __rmul__(self: 'variable[int]', other: int) -> 'variable[int]': ... + def __rmul__(self: 'value[int]', other: int) -> 'value[int]': ... @overload - def __rmul__(self, other: float) -> 'variable[float]': ... + def __rmul__(self, other: float) -> 'value[float]': ... def __rmul__(self, other: NumLike) -> Any: return self * other - def __truediv__(self, other: NumLike) -> 'variable[float]': + def __truediv__(self, other: NumLike) -> 'value[float]': return add_op('div', [self, other]) - def __rtruediv__(self, other: NumLike) -> 'variable[float]': + def __rtruediv__(self, other: NumLike) -> 'value[float]': return add_op('div', [other, self]) @overload - def __floordiv__(self: 'variable[TNum]', other: 'variable[TNum] | TNum') -> 'variable[TNum]': ... + def __floordiv__(self: 'value[TNum]', other: 'value[TNum] | TNum') -> 'value[TNum]': ... @overload - def __floordiv__(self: 'variable[int]', other: uniint) -> 'variable[int]': ... + def __floordiv__(self: 'value[int]', other: uniint) -> 'value[int]': ... @overload - def __floordiv__(self, other: unifloat) -> 'variable[float]': ... + def __floordiv__(self, other: unifloat) -> 'value[float]': ... @overload - def __floordiv__(self: 'variable[float]', other: NumLike) -> 'variable[float]': ... + def __floordiv__(self: 'value[float]', other: NumLike) -> 'value[float]': ... @overload - def __floordiv__(self, other: TVarNumb) -> 'variable[float] | variable[int]': ... + def __floordiv__(self, other: TVarNumb) -> 'value[float] | value[int]': ... def __floordiv__(self, other: TVarNumb) -> Any: return add_op('floordiv', [self, other]) @overload - def __rfloordiv__(self: 'variable[TNum]', other: TNum) -> 'variable[TNum]': ... + def __rfloordiv__(self: 'value[TNum]', other: TNum) -> 'value[TNum]': ... @overload - def __rfloordiv__(self: 'variable[int]', other: int) -> 'variable[int]': ... + def __rfloordiv__(self: 'value[int]', other: int) -> 'value[int]': ... @overload - def __rfloordiv__(self, other: float) -> 'variable[float]': ... + def __rfloordiv__(self, other: float) -> 'value[float]': ... def __rfloordiv__(self, other: NumLike) -> Any: return add_op('floordiv', [other, self]) def __neg__(self: TCPNum) -> TCPNum: if self.dtype == 'int': - return cast(TCPNum, add_op('sub', [variable(0), self])) - return cast(TCPNum, add_op('sub', [variable(0.0), self])) + return cast(TCPNum, add_op('sub', [value(0), self])) + return cast(TCPNum, add_op('sub', [value(0.0), self])) - def __gt__(self, other: TVarNumb) -> 'variable[int]': + def __gt__(self, other: TVarNumb) -> 'value[int]': ret = add_op('gt', [self, other]) - return variable(ret.source, dtype='bool') + return value(ret.source, dtype='bool') - def __lt__(self, other: TVarNumb) -> 'variable[int]': + def __lt__(self, other: TVarNumb) -> 'value[int]': ret = add_op('gt', [other, self]) - return variable(ret.source, dtype='bool') + return value(ret.source, dtype='bool') - def __ge__(self, other: TVarNumb) -> 'variable[int]': + def __ge__(self, other: TVarNumb) -> 'value[int]': ret = add_op('ge', [self, other]) - return variable(ret.source, dtype='bool') + return value(ret.source, dtype='bool') - def __le__(self, other: TVarNumb) -> 'variable[int]': + def __le__(self, other: TVarNumb) -> 'value[int]': ret = add_op('ge', [other, self]) - return variable(ret.source, dtype='bool') + return value(ret.source, dtype='bool') - def __eq__(self, other: TVarNumb) -> 'variable[int]': # type: ignore + def __eq__(self, other: TVarNumb) -> 'value[int]': # type: ignore ret = add_op('eq', [self, other], True) - return variable(ret.source, dtype='bool') + return value(ret.source, dtype='bool') - def __ne__(self, other: TVarNumb) -> 'variable[int]': # type: ignore + def __ne__(self, other: TVarNumb) -> 'value[int]': # type: ignore ret = add_op('ne', [self, other], True) - return variable(ret.source, dtype='bool') + return value(ret.source, dtype='bool') @overload - def __mod__(self: 'variable[TNum]', other: 'variable[TNum] | TNum') -> 'variable[TNum]': ... + def __mod__(self: 'value[TNum]', other: 'value[TNum] | TNum') -> 'value[TNum]': ... @overload - def __mod__(self: 'variable[int]', other: uniint) -> 'variable[int]': ... + def __mod__(self: 'value[int]', other: uniint) -> 'value[int]': ... @overload - def __mod__(self, other: unifloat) -> 'variable[float]': ... + def __mod__(self, other: unifloat) -> 'value[float]': ... @overload - def __mod__(self: 'variable[float]', other: NumLike) -> 'variable[float]': ... + def __mod__(self: 'value[float]', other: NumLike) -> 'value[float]': ... @overload - def __mod__(self, other: TVarNumb) -> 'variable[float] | variable[int]': ... + def __mod__(self, other: TVarNumb) -> 'value[float] | value[int]': ... def __mod__(self, other: TVarNumb) -> Any: return add_op('mod', [self, other]) @overload - def __rmod__(self: 'variable[TNum]', other: TNum) -> 'variable[TNum]': ... + def __rmod__(self: 'value[TNum]', other: TNum) -> 'value[TNum]': ... @overload - def __rmod__(self: 'variable[int]', other: int) -> 'variable[int]': ... + def __rmod__(self: 'value[int]', other: int) -> 'value[int]': ... @overload - def __rmod__(self, other: float) -> 'variable[float]': ... + def __rmod__(self, other: float) -> 'value[float]': ... def __rmod__(self, other: NumLike) -> Any: return add_op('mod', [other, self]) @overload - def __pow__(self: 'variable[TNum]', other: 'variable[TNum] | TNum') -> 'variable[TNum]': ... + def __pow__(self: 'value[TNum]', other: 'value[TNum] | TNum') -> 'value[TNum]': ... @overload - def __pow__(self: 'variable[int]', other: uniint) -> 'variable[int]': ... + def __pow__(self: 'value[int]', other: uniint) -> 'value[int]': ... @overload - def __pow__(self, other: unifloat) -> 'variable[float]': ... + def __pow__(self, other: unifloat) -> 'value[float]': ... @overload - def __pow__(self: 'variable[float]', other: NumLike) -> 'variable[float]': ... + def __pow__(self: 'value[float]', other: NumLike) -> 'value[float]': ... @overload - def __pow__(self, other: TVarNumb) -> 'variable[float] | variable[int]': ... + def __pow__(self, other: TVarNumb) -> 'value[float] | value[int]': ... def __pow__(self, other: TVarNumb) -> Any: return cp.pow(self, other) @overload - def __rpow__(self: 'variable[TNum]', other: TNum) -> 'variable[TNum]': ... + def __rpow__(self: 'value[TNum]', other: TNum) -> 'value[TNum]': ... @overload - def __rpow__(self: 'variable[int]', other: int) -> 'variable[int]': ... + def __rpow__(self: 'value[int]', other: int) -> 'value[int]': ... @overload - def __rpow__(self, other: float) -> 'variable[float]': ... + def __rpow__(self, other: float) -> 'value[float]': ... def __rpow__(self, other: NumLike) -> Any: return cp.pow(other, self) @@ -296,34 +296,34 @@ class variable(Generic[TNum], Net): return super().__hash__() # Bitwise and shift operations for cp[int] - def __lshift__(self, other: uniint) -> 'variable[int]': + def __lshift__(self, other: uniint) -> 'value[int]': return add_op('lshift', [self, other]) - def __rlshift__(self, other: uniint) -> 'variable[int]': + def __rlshift__(self, other: uniint) -> 'value[int]': return add_op('lshift', [other, self]) - def __rshift__(self, other: uniint) -> 'variable[int]': + def __rshift__(self, other: uniint) -> 'value[int]': return add_op('rshift', [self, other]) - def __rrshift__(self, other: uniint) -> 'variable[int]': + def __rrshift__(self, other: uniint) -> 'value[int]': return add_op('rshift', [other, self]) - def __and__(self, other: uniint) -> 'variable[int]': + def __and__(self, other: uniint) -> 'value[int]': return add_op('bwand', [self, other], True) - def __rand__(self, other: uniint) -> 'variable[int]': + def __rand__(self, other: uniint) -> 'value[int]': return add_op('bwand', [other, self], True) - def __or__(self, other: uniint) -> 'variable[int]': + def __or__(self, other: uniint) -> 'value[int]': return add_op('bwor', [self, other], True) - def __ror__(self, other: uniint) -> 'variable[int]': + def __ror__(self, other: uniint) -> 'value[int]': return add_op('bwor', [other, self], True) - def __xor__(self, other: uniint) -> 'variable[int]': + def __xor__(self, other: uniint) -> 'value[int]': return add_op('bwxor', [self, other], True) - def __rxor__(self, other: uniint) -> 'variable[int]': + def __rxor__(self, other: uniint) -> 'value[int]': return add_op('bwxor', [other, self], True) @@ -356,30 +356,30 @@ class Op(Node): self.node_hash = self.get_node_hash(commutative) -def net_from_value(value: Any) -> variable[Any]: - vi = CPConstant(value) - return variable(vi, vi.dtype) +def net_from_value(val: Any) -> value[Any]: + vi = CPConstant(val) + return value(vi, vi.dtype) @overload -def iif(expression: variable[Any], true_result: uniint, false_result: uniint) -> variable[int]: ... # pyright: ignore[reportOverlappingOverload] +def iif(expression: value[Any], true_result: uniint, false_result: uniint) -> value[int]: ... # pyright: ignore[reportOverlappingOverload] @overload -def iif(expression: variable[Any], true_result: unifloat, false_result: unifloat) -> variable[float]: ... +def iif(expression: value[Any], true_result: unifloat, false_result: unifloat) -> value[float]: ... @overload def iif(expression: float | int, true_result: TNum, false_result: TNum) -> TNum: ... @overload -def iif(expression: float | int, true_result: TNum | variable[TNum], false_result: variable[TNum]) -> variable[TNum]: ... +def iif(expression: float | int, true_result: TNum | value[TNum], false_result: value[TNum]) -> value[TNum]: ... @overload -def iif(expression: float | int, true_result: variable[TNum], false_result: TNum | variable[TNum]) -> variable[TNum]: ... +def iif(expression: float | int, true_result: value[TNum], false_result: TNum | value[TNum]) -> value[TNum]: ... @overload -def iif(expression: float | int | variable[Any], true_result: TNum | variable[TNum], false_result: TNum | variable[TNum]) -> variable[TNum] | TNum: ... +def iif(expression: float | int | value[Any], true_result: TNum | value[TNum], false_result: TNum | value[TNum]) -> value[TNum] | TNum: ... def iif(expression: Any, true_result: Any, false_result: Any) -> Any: - allowed_type = (variable, int, float) + allowed_type = (value, int, float) assert isinstance(true_result, allowed_type) and isinstance(false_result, allowed_type), "Result type not supported" return (expression != 0) * true_result + (expression == 0) * false_result -def add_op(op: str, args: list[variable[Any] | int | float], commutative: bool = False) -> variable[Any]: +def add_op(op: str, args: list[value[Any] | int | float], commutative: bool = False) -> value[Any]: arg_nets = [a if isinstance(a, Net) else net_from_value(a) for a in args] if commutative: @@ -393,9 +393,9 @@ def add_op(op: str, args: list[variable[Any] | int | float], commutative: bool = result_type = generic_sdb.stencil_definitions[typed_op].split('_')[0] if result_type == 'float': - return variable[float](Op(typed_op, arg_nets, commutative), result_type) + return value[float](Op(typed_op, arg_nets, commutative), result_type) else: - return variable[int](Op(typed_op, arg_nets, commutative), result_type) + return value[int](Op(typed_op, arg_nets, commutative), result_type) def _get_data_and_dtype(value: Any) -> tuple[str, float | int]: diff --git a/src/copapy/_math.py b/src/copapy/_math.py index 03a22bf..3303ce1 100644 --- a/src/copapy/_math.py +++ b/src/copapy/_math.py @@ -1,18 +1,18 @@ from . import vector from ._vectors import VecNumLike -from . import variable, NumLike +from . import value, NumLike from typing import TypeVar, Any, overload, Callable from ._basic_types import add_op, unifloat import math -T = TypeVar("T", int, float, variable[int], variable[float]) +T = TypeVar("T", int, float, value[int], value[float]) U = TypeVar("U", int, float) @overload def exp(x: float | int) -> float: ... @overload -def exp(x: variable[Any]) -> variable[float]: ... +def exp(x: value[Any]) -> value[float]: ... @overload def exp(x: vector[Any]) -> vector[float]: ... def exp(x: Any) -> Any: @@ -24,7 +24,7 @@ def exp(x: Any) -> Any: Returns: result of e**x """ - if isinstance(x, variable): + if isinstance(x, value): return add_op('exp', [x]) if isinstance(x, vector): return x.map(exp) @@ -34,7 +34,7 @@ def exp(x: Any) -> Any: @overload def log(x: float | int) -> float: ... @overload -def log(x: variable[Any]) -> variable[float]: ... +def log(x: value[Any]) -> value[float]: ... @overload def log(x: vector[Any]) -> vector[float]: ... def log(x: Any) -> Any: @@ -46,7 +46,7 @@ def log(x: Any) -> Any: Returns: result of ln(x) """ - if isinstance(x, variable): + if isinstance(x, value): return add_op('log', [x]) if isinstance(x, vector): return x.map(log) @@ -56,9 +56,9 @@ def log(x: Any) -> Any: @overload def pow(x: float | int, y: float | int) -> float: ... @overload -def pow(x: variable[Any], y: NumLike) -> variable[float]: ... +def pow(x: value[Any], y: NumLike) -> value[float]: ... @overload -def pow(x: NumLike, y: variable[Any]) -> variable[float]: ... +def pow(x: NumLike, y: value[Any]) -> value[float]: ... @overload def pow(x: vector[Any], y: Any) -> vector[float]: ... def pow(x: VecNumLike, y: VecNumLike) -> Any: @@ -81,7 +81,7 @@ def pow(x: VecNumLike, y: VecNumLike) -> Any: return m if y == -1: return 1 / x - if isinstance(x, variable) or isinstance(y, variable): + if isinstance(x, value) or isinstance(y, value): return add_op('pow', [x, y]) else: return float(x ** y) @@ -90,7 +90,7 @@ def pow(x: VecNumLike, y: VecNumLike) -> Any: @overload def sqrt(x: float | int) -> float: ... @overload -def sqrt(x: variable[Any]) -> variable[float]: ... +def sqrt(x: value[Any]) -> value[float]: ... @overload def sqrt(x: vector[Any]) -> vector[float]: ... def sqrt(x: Any) -> Any: @@ -102,7 +102,7 @@ def sqrt(x: Any) -> Any: Returns: Square root of x """ - if isinstance(x, variable): + if isinstance(x, value): return add_op('sqrt', [x]) if isinstance(x, vector): return x.map(sqrt) @@ -112,7 +112,7 @@ def sqrt(x: Any) -> Any: @overload def sin(x: float | int) -> float: ... @overload -def sin(x: variable[Any]) -> variable[float]: ... +def sin(x: value[Any]) -> value[float]: ... @overload def sin(x: vector[Any]) -> vector[float]: ... def sin(x: Any) -> Any: @@ -124,7 +124,7 @@ def sin(x: Any) -> Any: Returns: Square root of x """ - if isinstance(x, variable): + if isinstance(x, value): return add_op('sin', [x]) if isinstance(x, vector): return x.map(sin) @@ -134,7 +134,7 @@ def sin(x: Any) -> Any: @overload def cos(x: float | int) -> float: ... @overload -def cos(x: variable[Any]) -> variable[float]: ... +def cos(x: value[Any]) -> value[float]: ... @overload def cos(x: vector[Any]) -> vector[float]: ... def cos(x: Any) -> Any: @@ -146,7 +146,7 @@ def cos(x: Any) -> Any: Returns: Cosine of x """ - if isinstance(x, variable): + if isinstance(x, value): return add_op('cos', [x]) if isinstance(x, vector): return x.map(cos) @@ -156,7 +156,7 @@ def cos(x: Any) -> Any: @overload def tan(x: float | int) -> float: ... @overload -def tan(x: variable[Any]) -> variable[float]: ... +def tan(x: value[Any]) -> value[float]: ... @overload def tan(x: vector[Any]) -> vector[float]: ... def tan(x: Any) -> Any: @@ -168,7 +168,7 @@ def tan(x: Any) -> Any: Returns: Tangent of x """ - if isinstance(x, variable): + if isinstance(x, value): return add_op('tan', [x]) if isinstance(x, vector): #return x.map(tan) @@ -179,7 +179,7 @@ def tan(x: Any) -> Any: @overload def atan(x: float | int) -> float: ... @overload -def atan(x: variable[Any]) -> variable[float]: ... +def atan(x: value[Any]) -> value[float]: ... @overload def atan(x: vector[Any]) -> vector[float]: ... def atan(x: Any) -> Any: @@ -191,7 +191,7 @@ def atan(x: Any) -> Any: Returns: Inverse tangent of x """ - if isinstance(x, variable): + if isinstance(x, value): return add_op('atan', [x]) if isinstance(x, vector): return x.map(atan) @@ -201,9 +201,9 @@ def atan(x: Any) -> Any: @overload def atan2(x: float | int, y: float | int) -> float: ... @overload -def atan2(x: variable[Any], y: NumLike) -> variable[float]: ... +def atan2(x: value[Any], y: NumLike) -> value[float]: ... @overload -def atan2(x: NumLike, y: variable[Any]) -> variable[float]: ... +def atan2(x: NumLike, y: value[Any]) -> value[float]: ... @overload def atan2(x: vector[float], y: VecNumLike) -> vector[float]: ... @overload @@ -220,7 +220,7 @@ def atan2(x: VecNumLike, y: VecNumLike) -> Any: """ if isinstance(x, vector) or isinstance(y, vector): return _map2(x, y, atan2) - if isinstance(x, variable) or isinstance(y, variable): + if isinstance(x, value) or isinstance(y, value): return add_op('atan2', [x, y]) return math.atan2(x, y) @@ -228,7 +228,7 @@ def atan2(x: VecNumLike, y: VecNumLike) -> Any: @overload def asin(x: float | int) -> float: ... @overload -def asin(x: variable[Any]) -> variable[float]: ... +def asin(x: value[Any]) -> value[float]: ... @overload def asin(x: vector[Any]) -> vector[float]: ... def asin(x: Any) -> Any: @@ -240,7 +240,7 @@ def asin(x: Any) -> Any: Returns: Inverse sine of x """ - if isinstance(x, variable): + if isinstance(x, value): return add_op('asin', [x]) if isinstance(x, vector): return x.map(asin) @@ -250,7 +250,7 @@ def asin(x: Any) -> Any: @overload def acos(x: float | int) -> float: ... @overload -def acos(x: variable[Any]) -> variable[float]: ... +def acos(x: value[Any]) -> value[float]: ... @overload def acos(x: vector[Any]) -> vector[float]: ... def acos(x: Any) -> Any: @@ -262,7 +262,7 @@ def acos(x: Any) -> Any: Returns: Inverse cosine of x """ - if isinstance(x, variable): + if isinstance(x, value): return add_op('acos', [x]) if isinstance(x, vector): return x.map(acos) @@ -272,10 +272,10 @@ def acos(x: Any) -> Any: @overload def get_42(x: float | int) -> float: ... @overload -def get_42(x: variable[Any]) -> variable[float]: ... -def get_42(x: NumLike) -> variable[float] | float: - """Returns the variable representing the constant 42""" - if isinstance(x, variable): +def get_42(x: value[Any]) -> value[float]: ... +def get_42(x: NumLike) -> value[float] | float: + """Returns the value representing the constant 42""" + if isinstance(x, value): return add_op('get_42', [x, x]) return float((int(x) * 3.0 + 42.0) * 5.0 + 21.0) @@ -284,10 +284,10 @@ def get_42(x: NumLike) -> variable[float] | float: @overload def abs(x: U) -> U: ... @overload -def abs(x: variable[U]) -> variable[U]: ... +def abs(x: value[U]) -> value[U]: ... @overload def abs(x: vector[U]) -> vector[U]: ... -def abs(x: U | variable[U] | vector[U]) -> Any: +def abs(x: U | value[U] | vector[U]) -> Any: """Absolute value function Arguments: @@ -304,10 +304,10 @@ def abs(x: U | variable[U] | vector[U]) -> Any: @overload def sign(x: U) -> U: ... @overload -def sign(x: variable[U]) -> variable[U]: ... +def sign(x: value[U]) -> value[U]: ... @overload def sign(x: vector[U]) -> vector[U]: ... -def sign(x: U | variable[U] | vector[U]) -> Any: +def sign(x: U | value[U] | vector[U]) -> Any: """Return 1 for positive numbers and -1 for negative numbers. For an input of 0 the return value is 0. @@ -322,16 +322,16 @@ def sign(x: U | variable[U] | vector[U]) -> Any: @overload -def clamp(x: variable[U], min_value: U | variable[U], max_value: U | variable[U]) -> variable[U]: ... +def clamp(x: value[U], min_value: U | value[U], max_value: U | value[U]) -> value[U]: ... @overload -def clamp(x: U | variable[U], min_value: variable[U], max_value: U | variable[U]) -> variable[U]: ... +def clamp(x: U | value[U], min_value: value[U], max_value: U | value[U]) -> value[U]: ... @overload -def clamp(x: U | variable[U], min_value: U | variable[U], max_value: variable[U]) -> variable[U]: ... +def clamp(x: U | value[U], min_value: U | value[U], max_value: value[U]) -> value[U]: ... @overload def clamp(x: U, min_value: U, max_value: U) -> U: ... @overload -def clamp(x: vector[U], min_value: 'U | variable[U]', max_value: 'U | variable[U]') -> vector[U]: ... -def clamp(x: U | variable[U] | vector[U], min_value: U | variable[U], max_value: U | variable[U]) -> Any: +def clamp(x: vector[U], min_value: 'U | value[U]', max_value: 'U | value[U]') -> vector[U]: ... +def clamp(x: U | value[U] | vector[U], min_value: U | value[U], max_value: U | value[U]) -> Any: """Clamp function to limit a value between a minimum and maximum. Arguments: @@ -351,12 +351,12 @@ def clamp(x: U | variable[U] | vector[U], min_value: U | variable[U], max_value: @overload -def min(x: variable[U], y: U | variable[U]) -> variable[U]: ... +def min(x: value[U], y: U | value[U]) -> value[U]: ... @overload -def min(x: U | variable[U], y: variable[U]) -> variable[U]: ... +def min(x: U | value[U], y: value[U]) -> value[U]: ... @overload def min(x: U, y: U) -> U: ... -def min(x: U | variable[U], y: U | variable[U]) -> Any: +def min(x: U | value[U], y: U | value[U]) -> Any: """Minimum function to get the smaller of two values. Arguments: @@ -370,12 +370,12 @@ def min(x: U | variable[U], y: U | variable[U]) -> Any: @overload -def max(x: variable[U], y: U | variable[U]) -> variable[U]: ... +def max(x: value[U], y: U | value[U]) -> value[U]: ... @overload -def max(x: U | variable[U], y: variable[U]) -> variable[U]: ... +def max(x: U | value[U], y: value[U]) -> value[U]: ... @overload def max(x: U, y: U) -> U: ... -def max(x: U | variable[U], y: U | variable[U]) -> Any: +def max(x: U | value[U], y: U | value[U]) -> Any: """Maximum function to get the larger of two values. Arguments: @@ -389,16 +389,16 @@ def max(x: U | variable[U], y: U | variable[U]) -> Any: @overload -def lerp(v1: variable[U], v2: U | variable[U], t: unifloat) -> variable[U]: ... +def lerp(v1: value[U], v2: U | value[U], t: unifloat) -> value[U]: ... @overload -def lerp(v1: U | variable[U], v2: variable[U], t: unifloat) -> variable[U]: ... +def lerp(v1: U | value[U], v2: value[U], t: unifloat) -> value[U]: ... @overload -def lerp(v1: U | variable[U], v2: U | variable[U], t: variable[float]) -> variable[U]: ... +def lerp(v1: U | value[U], v2: U | value[U], t: value[float]) -> value[U]: ... @overload def lerp(v1: U, v2: U, t: float) -> U: ... @overload def lerp(v1: vector[U], v2: vector[U], t: unifloat) -> vector[U]: ... -def lerp(v1: U | variable[U] | vector[U], v2: U | variable[U] | vector[U], t: unifloat) -> Any: +def lerp(v1: U | value[U] | vector[U], v2: U | value[U] | vector[U], t: unifloat) -> Any: """Linearly interpolate between two values or vectors v1 and v2 by a factor t.""" if isinstance(v1, vector) or isinstance(v2, vector): assert isinstance(v1, vector) and isinstance(v2, vector), "None or both v1 and v2 must be vectors." @@ -410,16 +410,16 @@ def lerp(v1: U | variable[U] | vector[U], v2: U | variable[U] | vector[U], t: u @overload def relu(x: U) -> U: ... @overload -def relu(x: variable[U]) -> variable[U]: ... +def relu(x: value[U]) -> value[U]: ... @overload def relu(x: vector[U]) -> vector[U]: ... -def relu(x: U | variable[U] | vector[U]) -> Any: +def relu(x: U | value[U] | vector[U]) -> Any: """Returns x for x > 0 and otherwise 0.""" ret = (x > 0) * x return ret -def _map2(self: VecNumLike, other: VecNumLike, func: Callable[[Any, Any], variable[U] | U]) -> vector[U]: +def _map2(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): return vector(func(x, y) for x, y in zip(self.values, other.values)) diff --git a/src/copapy/_matrices.py b/src/copapy/_matrices.py index 773ac38..a5505a7 100644 --- a/src/copapy/_matrices.py +++ b/src/copapy/_matrices.py @@ -1,23 +1,23 @@ -from . import variable +from . import value from ._vectors import vector from ._mixed import mixed_sum from typing import TypeVar, Iterable, Any, overload, TypeAlias, Callable, Iterator, Generic from ._helper_types import TNum -MatNumLike: TypeAlias = 'matrix[int] | matrix[float] | variable[int] | variable[float] | int | float' -MatIntLike: TypeAlias = 'matrix[int] | variable[int] | int' -MatFloatLike: TypeAlias = 'matrix[float] | variable[float] | float' +MatNumLike: TypeAlias = 'matrix[int] | matrix[float] | value[int] | value[float] | int | float' +MatIntLike: TypeAlias = 'matrix[int] | value[int] | int' +MatFloatLike: TypeAlias = 'matrix[float] | value[float] | float' U = TypeVar("U", int, float) class matrix(Generic[TNum]): - """Mathematical matrix class supporting basic operations and interactions with variables. + """Mathematical matrix class supporting basic operations and interactions with values. """ - def __init__(self, values: Iterable[Iterable[TNum | variable[TNum]]] | vector[TNum]): - """Create a matrix with given values and variables. + def __init__(self, values: Iterable[Iterable[TNum | value[TNum]]] | vector[TNum]): + """Create a matrix with given values. Args: - values: iterable of iterable of constant values and variables + values: iterable of iterable of constant values """ if isinstance(values, vector): rows = [values.values] @@ -27,7 +27,7 @@ class matrix(Generic[TNum]): if rows: row_len = len(rows[0]) assert all(len(row) == row_len for row in rows), "All rows must have the same length" - self.values: tuple[tuple[variable[TNum] | TNum, ...], ...] = tuple(rows) + self.values: tuple[tuple[value[TNum] | TNum, ...], ...] = tuple(rows) self.rows = len(self.values) self.cols = len(self.values[0]) if self.values else 0 @@ -41,7 +41,7 @@ class matrix(Generic[TNum]): @overload def __getitem__(self, key: int) -> vector[TNum]: ... @overload - def __getitem__(self, key: tuple[int, int]) -> variable[TNum] | TNum: ... + def __getitem__(self, key: tuple[int, int]) -> value[TNum] | TNum: ... def __getitem__(self, key: int | tuple[int, int]) -> Any: """Get a row as a vector or a specific element. Args: @@ -56,7 +56,7 @@ class matrix(Generic[TNum]): else: return vector(self.values[key]) - def __iter__(self) -> Iterator[tuple[variable[TNum] | TNum, ...]]: + def __iter__(self) -> Iterator[tuple[value[TNum] | TNum, ...]]: return iter(self.values) def __neg__(self) -> 'matrix[TNum]': @@ -86,7 +86,7 @@ class matrix(Generic[TNum]): @overload def __radd__(self: 'matrix[float]', other: MatNumLike) -> 'matrix[float]': ... @overload - def __radd__(self: 'matrix[int]', other: variable[int] | int) -> 'matrix[int]': ... + def __radd__(self: 'matrix[int]', other: value[int] | int) -> 'matrix[int]': ... def __radd__(self, other: Any) -> Any: return self + other @@ -114,7 +114,7 @@ class matrix(Generic[TNum]): @overload def __rsub__(self: 'matrix[float]', other: MatNumLike) -> 'matrix[float]': ... @overload - def __rsub__(self: 'matrix[int]', other: variable[int] | int) -> 'matrix[int]': ... + def __rsub__(self: 'matrix[int]', other: value[int] | int) -> 'matrix[int]': ... def __rsub__(self, other: MatNumLike) -> Any: if isinstance(other, matrix): assert self.rows == other.rows and self.cols == other.cols, \ @@ -153,7 +153,7 @@ class matrix(Generic[TNum]): @overload def __rmul__(self: 'matrix[float]', other: MatNumLike) -> 'matrix[float]': ... @overload - def __rmul__(self: 'matrix[int]', other: variable[int] | int) -> 'matrix[int]': ... + def __rmul__(self: 'matrix[int]', other: value[int] | int) -> 'matrix[int]': ... def __rmul__(self, other: MatNumLike) -> Any: return self * other @@ -199,9 +199,9 @@ class matrix(Generic[TNum]): assert isinstance(other, matrix), "Cannot multiply matrix with {type(other)}" assert self.cols == other.rows, \ f"Matrix columns ({self.cols}) must match other matrix rows ({other.rows})" - result: list[list[TNum | variable[TNum]]] = [] + result: list[list[TNum | value[TNum]]] = [] for row in self.values: - new_row: list[TNum | variable[TNum]] = [] + new_row: list[TNum | value[TNum]] = [] for col_idx in range(other.cols): col = tuple(other.values[i][col_idx] for i in range(other.rows)) element = sum(a * b for a, b in zip(row, col)) @@ -238,27 +238,27 @@ class matrix(Generic[TNum]): return vector(self.values[i][index] for i in range(self.rows)) @overload - def trace(self: 'matrix[TNum]') -> TNum | variable[TNum]: ... + def trace(self: 'matrix[TNum]') -> TNum | value[TNum]: ... @overload - def trace(self: 'matrix[int]') -> int | variable[int]: ... + def trace(self: 'matrix[int]') -> int | value[int]: ... @overload - def trace(self: 'matrix[float]') -> float | variable[float]: ... + def trace(self: 'matrix[float]') -> float | value[float]: ... def trace(self) -> Any: """Calculate the trace (sum of diagonal elements).""" assert self.rows == self.cols, "Trace is only defined for square matrices" return mixed_sum(self.values[i][i] for i in range(self.rows)) @overload - def sum(self: 'matrix[TNum]') -> TNum | variable[TNum]: ... + def sum(self: 'matrix[TNum]') -> TNum | value[TNum]: ... @overload - def sum(self: 'matrix[int]') -> int | variable[int]: ... + def sum(self: 'matrix[int]') -> int | value[int]: ... @overload - def sum(self: 'matrix[float]') -> float | variable[float]: ... + def sum(self: 'matrix[float]') -> float | value[float]: ... def sum(self) -> Any: """Calculate the sum of all elements.""" return mixed_sum(a for row in self.values for a in row) - def map(self, func: Callable[[Any], variable[U] | U]) -> 'matrix[U]': + def map(self, func: Callable[[Any], value[U] | U]) -> 'matrix[U]': """Applies a function to each element of the matrix and returns a new matrix.""" return matrix( tuple(func(a) for a in row) @@ -266,10 +266,10 @@ class matrix(Generic[TNum]): ) def homogenize(self) -> 'matrix[TNum]': - """Convert all elements to variables if any element is a variable.""" - if any(isinstance(val, variable) for row in self.values for val in row): + """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(variable(val) if not isinstance(val, variable) else val for val in row) + tuple(value(val) if not isinstance(val, value) else val for val in row) for row in self.values ) else: diff --git a/src/copapy/_mixed.py b/src/copapy/_mixed.py index e4d2632..5b84e93 100644 --- a/src/copapy/_mixed.py +++ b/src/copapy/_mixed.py @@ -1,24 +1,24 @@ -from . import variable +from . import value from typing import TypeVar, Iterable, Any, overload T = TypeVar("T", int, float) @overload -def mixed_sum(scalars: Iterable[float | variable[float]]) -> float | variable[float]: ... +def mixed_sum(scalars: Iterable[float | value[float]]) -> float | value[float]: ... @overload -def mixed_sum(scalars: Iterable[int | variable[int]]) -> int | variable[int]: ... +def mixed_sum(scalars: Iterable[int | value[int]]) -> int | value[int]: ... @overload -def mixed_sum(scalars: Iterable[T | variable[T]]) -> T | variable[T]: ... -def mixed_sum(scalars: Iterable[int | float | variable[Any]]) -> Any: +def mixed_sum(scalars: Iterable[T | value[T]]) -> T | value[T]: ... +def mixed_sum(scalars: Iterable[int | float | value[Any]]) -> Any: sl = list(scalars) - return sum(a for a in sl if not isinstance(a, variable)) +\ - sum(a for a in sl if isinstance(a, variable)) + return sum(a for a in sl if not isinstance(a, value)) +\ + sum(a for a in sl if isinstance(a, value)) -def mixed_homogenize(scalars: Iterable[T | variable[T]]) -> Iterable[T] | Iterable[variable[T]]: - if any(isinstance(val, variable) for val in scalars): - return (variable(val) if not isinstance(val, variable) else val for val in scalars) +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) else: - return (val for val in scalars if not isinstance(val, variable)) + return (val for val in scalars if not isinstance(val, value)) diff --git a/src/copapy/_target.py b/src/copapy/_target.py index d2bbed4..be0e6a1 100644 --- a/src/copapy/_target.py +++ b/src/copapy/_target.py @@ -3,7 +3,7 @@ from . import _binwrite as binw from coparun_module import coparun, read_data_mem import struct from ._basic_types import stencil_db_from_package -from ._basic_types import variable, Net, Node, Write, NumLike +from ._basic_types import value, Net, Node, Write, NumLike from ._compiler import compile_to_dag T = TypeVar("T", int, float) @@ -28,16 +28,16 @@ class Target(): optimization: Optimization level """ self.sdb = stencil_db_from_package(arch, optimization) - self._variables: dict[Net, tuple[int, int, str]] = {} + self._values: dict[Net, tuple[int, int, str]] = {} - def compile(self, *variables: int | float | variable[int] | variable[float] | Iterable[int | float | variable[int] | variable[float]]) -> None: - """Compiles the code to compute the given variables. + def compile(self, *values: int | float | value[int] | value[float] | Iterable[int | float | value[int] | value[float]]) -> None: + """Compiles the code to compute the given values. Arguments: - variables: Variables to compute + values: Values to compute """ nodes: list[Node] = [] - for s in variables: + for s in values: if isinstance(s, Iterable): for net in s: if isinstance(net, Net): @@ -46,7 +46,7 @@ class Target(): if isinstance(s, Net): nodes.append(Write(s)) - dw, self._variables = compile_to_dag(nodes, self.sdb) + dw, self._values = compile_to_dag(nodes, self.sdb) dw.write_com(binw.Command.END_COM) assert coparun(dw.get_data()) > 0 @@ -59,56 +59,56 @@ class Target(): assert coparun(dw.get_data()) > 0 @overload - def read_value(self, net: variable[T]) -> T: ... + def read_value(self, net: value[T]) -> T: ... @overload def read_value(self, net: NumLike) -> float | int | bool: ... @overload - def read_value(self, net: Iterable[T | variable[T]]) -> list[T]: ... - def read_value(self, net: NumLike | variable[T] | Iterable[T | variable[T]]) -> Any: - """Reads the value of a variable. + def read_value(self, net: Iterable[T | value[T]]) -> list[T]: ... + def read_value(self, net: NumLike | value[T] | Iterable[T | value[T]]) -> Any: + """Reads the numeric value of a copapy type. Arguments: - net: Variable to read + net: Values to read Returns: - Value of the variable + Numeric value """ if isinstance(net, Iterable): - return [self.read_value(ni) if isinstance(ni, variable) else ni for ni in net] + return [self.read_value(ni) if isinstance(ni, value) else ni for ni in net] if isinstance(net, float | int): print("Warning: value is not a copypy value") return net - assert isinstance(net, Net), "Variable must be a copapy variable object" - assert net in self._variables, f"Variable {net} not found. It might not have been compiled for the target." - addr, lengths, var_type = self._variables[net] + assert isinstance(net, Net), "Argument must be a copapy value" + assert net in self._values, f"Value {net} not found. It might not have been compiled for the target." + addr, lengths, var_type = self._values[net] assert lengths > 0 data = read_data_mem(addr, lengths) - assert data is not None and len(data) == lengths, f"Failed to read variable {net}" + assert data is not None and len(data) == lengths, f"Failed to read value {net}" en = {'little': '<', 'big': '>'}[self.sdb.byteorder] if var_type == 'float': if lengths == 4: - value = struct.unpack(en + 'f', data)[0] + val = struct.unpack(en + 'f', data)[0] elif lengths == 8: - value = struct.unpack(en + 'd', data)[0] + val = struct.unpack(en + 'd', data)[0] else: raise ValueError(f"Unsupported float length: {lengths} bytes") - assert isinstance(value, float) - return value + assert isinstance(val, float) + return val elif var_type == 'int': assert lengths in (1, 2, 4, 8), f"Unsupported int length: {lengths} bytes" - value = int.from_bytes(data, byteorder=self.sdb.byteorder, signed=True) - return value + val = int.from_bytes(data, byteorder=self.sdb.byteorder, signed=True) + return val elif var_type == 'bool': assert lengths in (1, 2, 4, 8), f"Unsupported int length: {lengths} bytes" - value = bool.from_bytes(data, byteorder=self.sdb.byteorder, signed=True) - return value + val = bool.from_bytes(data, byteorder=self.sdb.byteorder, signed=True) + return val else: - raise ValueError(f"Unsupported variable type: {var_type}") + raise ValueError(f"Unsupported value type: {var_type}") def read_value_remote(self, net: Net) -> None: - """Reads the raw data of a variable by the runner.""" + """Reads the raw data of a value by the runner.""" dw = binw.data_writer(self.sdb.byteorder) - add_read_command(dw, self._variables, net) + add_read_command(dw, self._values, net) assert coparun(dw.get_data()) > 0 diff --git a/src/copapy/_vectors.py b/src/copapy/_vectors.py index 44d5869..ced1137 100644 --- a/src/copapy/_vectors.py +++ b/src/copapy/_vectors.py @@ -1,28 +1,28 @@ -from . import variable +from . import value from ._mixed import mixed_sum, mixed_homogenize from typing import TypeVar, Iterable, Any, overload, TypeAlias, Callable, Iterator, Generic import copapy as cp from ._helper_types import TNum -#VecNumLike: TypeAlias = 'vector[int] | vector[float] | variable[int] | variable[float] | int | float | bool' -VecNumLike: TypeAlias = 'vector[Any] | variable[Any] | int | float | bool' -VecIntLike: TypeAlias = 'vector[int] | variable[int] | int' -VecFloatLike: TypeAlias = 'vector[float] | variable[float] | float' +#VecNumLike: TypeAlias = 'vector[int] | vector[float] | value[int] | value[float] | int | float | bool' +VecNumLike: TypeAlias = 'vector[Any] | value[Any] | int | float | bool' +VecIntLike: TypeAlias = 'vector[int] | value[int] | int' +VecFloatLike: TypeAlias = 'vector[float] | value[float] | float' U = TypeVar("U", int, float) epsilon = 1e-20 class vector(Generic[TNum]): - """Mathematical vector class supporting basic operations and interactions with variables. + """Mathematical vector class supporting basic operations and interactions with values. """ - def __init__(self, values: Iterable[TNum | variable[TNum]]): - """Create a vector with given values and variables. + def __init__(self, values: Iterable[TNum | value[TNum]]): + """Create a vector with given values. Args: - values: iterable of constant values and variables + values: iterable of constant values """ - self.values: tuple[variable[TNum] | TNum, ...] = tuple(values) + self.values: tuple[value[TNum] | TNum, ...] = tuple(values) def __repr__(self) -> str: return f"vector({self.values})" @@ -31,10 +31,10 @@ class vector(Generic[TNum]): return len(self.values) @overload - def __getitem__(self, index: int) -> variable[TNum] | TNum: ... + def __getitem__(self, index: int) -> value[TNum] | TNum: ... @overload def __getitem__(self, index: slice) -> 'vector[TNum]': ... - def __getitem__(self, index: int | slice) -> 'vector[TNum] | variable[TNum] | TNum': + def __getitem__(self, index: int | slice) -> 'vector[TNum] | value[TNum] | TNum': if isinstance(index, slice): return vector(self.values[index]) return self.values[index] @@ -42,7 +42,7 @@ class vector(Generic[TNum]): def __neg__(self) -> 'vector[TNum]': return vector(-a for a in self.values) - def __iter__(self) -> Iterator[variable[TNum] | TNum]: + def __iter__(self) -> Iterator[value[TNum] | TNum]: return iter(self.values) @overload @@ -62,7 +62,7 @@ class vector(Generic[TNum]): @overload def __radd__(self: 'vector[float]', other: VecNumLike) -> 'vector[float]': ... @overload - def __radd__(self: 'vector[int]', other: variable[int] | int) -> 'vector[int]': ... + def __radd__(self: 'vector[int]', other: value[int] | int) -> 'vector[int]': ... @overload def __radd__(self, other: VecNumLike) -> 'vector[Any]': ... def __radd__(self, other: Any) -> Any: @@ -85,7 +85,7 @@ class vector(Generic[TNum]): @overload def __rsub__(self: 'vector[float]', other: VecNumLike) -> 'vector[float]': ... @overload - def __rsub__(self: 'vector[int]', other: variable[int] | int) -> 'vector[int]': ... + def __rsub__(self: 'vector[int]', other: value[int] | int) -> 'vector[int]': ... @overload def __rsub__(self, other: VecNumLike) -> 'vector[Any]': ... def __rsub__(self, other: VecNumLike) -> Any: @@ -111,7 +111,7 @@ class vector(Generic[TNum]): @overload def __rmul__(self: 'vector[float]', other: VecNumLike) -> 'vector[float]': ... @overload - def __rmul__(self: 'vector[int]', other: variable[int] | int) -> 'vector[int]': ... + def __rmul__(self: 'vector[int]', other: value[int] | int) -> 'vector[int]': ... @overload def __rmul__(self, other: VecNumLike) -> 'vector[Any]': ... def __rmul__(self, other: VecNumLike) -> Any: @@ -134,7 +134,7 @@ class vector(Generic[TNum]): @overload def __rpow__(self: 'vector[float]', other: VecNumLike) -> 'vector[float]': ... @overload - def __rpow__(self: 'vector[int]', other: variable[int] | int) -> 'vector[int]': ... + def __rpow__(self: 'vector[int]', other: value[int] | int) -> 'vector[int]': ... @overload def __rpow__(self, other: VecNumLike) -> 'vector[Any]': ... def __rpow__(self, other: VecNumLike) -> Any: @@ -153,26 +153,26 @@ class vector(Generic[TNum]): return vector(other / a for a in self.values) @overload - def dot(self: 'vector[int]', other: 'vector[int]') -> int | variable[int]: ... + def dot(self: 'vector[int]', other: 'vector[int]') -> int | value[int]: ... @overload - def dot(self, other: 'vector[float]') -> float | variable[float]: ... + def dot(self, other: 'vector[float]') -> float | value[float]: ... @overload - def dot(self: 'vector[float]', other: 'vector[int] | vector[float]') -> float | variable[float]: ... + def dot(self: 'vector[float]', other: 'vector[int] | vector[float]') -> float | value[float]: ... @overload - def dot(self, other: 'vector[int] | vector[float]') -> float | int | variable[float] | variable[int]: ... + def dot(self, other: 'vector[int] | vector[float]') -> float | int | value[float] | value[int]: ... def dot(self, other: 'vector[int] | vector[float]') -> Any: assert len(self.values) == len(other.values), "Vectors must be of same length." return mixed_sum(a * b for a, b in zip(self.values, other.values)) # @ operator @overload - def __matmul__(self: 'vector[int]', other: 'vector[int]') -> int | variable[int]: ... + def __matmul__(self: 'vector[int]', other: 'vector[int]') -> int | value[int]: ... @overload - def __matmul__(self, other: 'vector[float]') -> float | variable[float]: ... + def __matmul__(self, other: 'vector[float]') -> float | value[float]: ... @overload - def __matmul__(self: 'vector[float]', other: 'vector[int] | vector[float]') -> float | variable[float]: ... + def __matmul__(self: 'vector[float]', other: 'vector[int] | vector[float]') -> float | value[float]: ... @overload - def __matmul__(self, other: 'vector[int] | vector[float]') -> float | int | variable[float] | variable[int]: ... + def __matmul__(self, other: 'vector[int] | vector[float]') -> float | int | value[float] | value[int]: ... def __matmul__(self, other: 'vector[int] | vector[float]') -> Any: return self.dot(other) @@ -229,14 +229,14 @@ class vector(Generic[TNum]): return (len(self.values),) @overload - def sum(self: 'vector[int]') -> int | variable[int]: ... + def sum(self: 'vector[int]') -> int | value[int]: ... @overload - def sum(self: 'vector[float]') -> float | variable[float]: ... + def sum(self: 'vector[float]') -> float | value[float]: ... def sum(self) -> Any: """Sum of all vector elements.""" return mixed_sum(self.values) - def magnitude(self) -> 'float | variable[float]': + def magnitude(self) -> 'float | value[float]': """Magnitude (length) of the vector.""" s = mixed_sum(a * a for a in self.values) return cp.sqrt(s) @@ -247,12 +247,12 @@ class vector(Generic[TNum]): return self / mag def homogenize(self) -> 'vector[TNum]': - if any(isinstance(val, variable) for val in self.values): + if any(isinstance(val, value) for val in self.values): return vector(mixed_homogenize(self)) else: return self - def map(self, func: Callable[[Any], variable[U] | U]) -> 'vector[U]': + def map(self, func: Callable[[Any], value[U] | U]) -> 'vector[U]': """Applies a function to each element of the vector and returns a new vector.""" return vector(func(x) for x in self.values) @@ -262,18 +262,18 @@ def cross_product(v1: vector[float], v2: vector[float]) -> vector[float]: return v1.cross(v2) -def dot_product(v1: vector[float], v2: vector[float]) -> 'float | variable[float]': +def dot_product(v1: vector[float], v2: vector[float]) -> 'float | value[float]': """Calculate the dot product of two vectors.""" return v1.dot(v2) -def distance(v1: vector[float], v2: vector[float]) -> 'float | variable[float]': +def distance(v1: vector[float], v2: vector[float]) -> 'float | value[float]': """Calculate the Euclidean distance between two vectors.""" diff = v1 - v2 return diff.magnitude() -def scalar_projection(v1: vector[float], v2: vector[float]) -> 'float | variable[float]': +def scalar_projection(v1: vector[float], v2: vector[float]) -> 'float | value[float]': """Calculate the scalar projection of v1 onto v2.""" dot_prod = v1.dot(v2) mag_v2 = v2.magnitude() + epsilon @@ -288,7 +288,7 @@ def vector_projection(v1: vector[float], v2: vector[float]) -> vector[float]: return v2 * scalar_proj -def angle_between(v1: vector[float], v2: vector[float]) -> 'float | variable[float]': +def angle_between(v1: vector[float], v2: vector[float]) -> 'float | value[float]': """Calculate the angle in radians between two vectors.""" dot_prod = v1.dot(v2) mag_v1 = v1.magnitude() @@ -297,7 +297,7 @@ def angle_between(v1: vector[float], v2: vector[float]) -> 'float | variable[flo return cp.acos(cos_angle) -def rotate_vector(v: vector[float], axis: vector[float], angle: 'float | variable[float]') -> vector[float]: +def rotate_vector(v: vector[float], axis: vector[float], angle: 'float | value[float]') -> vector[float]: """Rotate vector v around a given axis by a specified angle using Rodrigues' rotation formula.""" k = axis.normalize() cos_angle = cp.cos(angle) diff --git a/src/copapy/filters.py b/src/copapy/filters.py index abd6d44..4cbdc40 100644 --- a/src/copapy/filters.py +++ b/src/copapy/filters.py @@ -1,15 +1,15 @@ -from . import variable, vector +from . import value, vector from ._basic_types import iif, unifloat from._helper_types import TNum from typing import Any, Iterable -def homogenize_vector(input_values: Iterable[TNum | variable[TNum]]) -> Iterable[TNum] | Iterable[variable[TNum]]: +def homogenize_vector(input_values: Iterable[TNum | value[TNum]]) -> Iterable[TNum] | Iterable[value[TNum]]: input_list = list(input_values) - if any(isinstance(val, variable) for val in input_list): - return (v if isinstance(v, variable) else variable(v) for v in input_list) + if any(isinstance(val, value) for val in input_list): + return (v if isinstance(v, value) else value(v) for v in input_list) else: - return (v for v in input_list if not isinstance(v, variable)) + return (v for v in input_list if not isinstance(v, value)) def _inv_argsort(input_vector: vector[TNum]) -> vector[int]: @@ -31,7 +31,7 @@ def argsort(input_vector: vector[TNum]) -> vector[int]: return _inv_argsort(_inv_argsort(input_vector)) -def median(input_vector: vector[TNum]) -> TNum | variable[TNum]: +def median(input_vector: vector[TNum]) -> TNum | value[TNum]: """ Applies a median filter to the input vector and returns the median as a unifloat. diff --git a/tests/benchmark.py b/tests/benchmark.py index b2d7f7b..647afd5 100644 --- a/tests/benchmark.py +++ b/tests/benchmark.py @@ -27,8 +27,8 @@ def cp_vs_python(path: str): #v_size = 400 iter_size = 30000 - v1 = cp.vector(cp.variable(float(v)) for v in range(v_size)) - v2 = cp.vector(cp.variable(float(v)) for v in [5]*v_size) + v1 = cp.vector(cp.value(float(v)) for v in range(v_size)) + v2 = cp.vector(cp.value(float(v)) for v in [5]*v_size) v3 = sum((v1 + i) @ v2 for i in range(sum_size)) @@ -95,8 +95,8 @@ def cp_vs_python_sparse(path: str = 'benchmark_results_001_sparse.json'): #v_size = 400 iter_size = 3000 - v1 = cp.vector(cp.variable(float(v)) for v in range(v_size)) - v2 = cp.vector(cp.variable(float(v)) for v in [5]*v_size) + v1 = cp.vector(cp.value(float(v)) for v in range(v_size)) + v2 = cp.vector(cp.value(float(v)) for v in [5]*v_size) test = cp.vector(np.linspace(0, 1, v_size)) diff --git a/tests/test_ast_gen.py b/tests/test_ast_gen.py index 89ee086..ee36f05 100644 --- a/tests/test_ast_gen.py +++ b/tests/test_ast_gen.py @@ -1,4 +1,4 @@ -from copapy import variable +from copapy import value from copapy.backend import Write import copapy.backend as cpb @@ -21,8 +21,8 @@ def test_ast_generation(): #r2 = i1 + 9 #out = [Write(r1), Write(r2)] - c1 = variable(4) - c2 = variable(2) + c1 = value(4) + c2 = value(2) #i1 = c1 * 2 #r1 = i1 + 7 + (c2 + 7 * 9) #r2 = i1 + 9 diff --git a/tests/test_autograd.py b/tests/test_autograd.py index f8ecaf1..841840f 100644 --- a/tests/test_autograd.py +++ b/tests/test_autograd.py @@ -1,4 +1,4 @@ -from copapy import variable, grad +from copapy import value, grad import copapy as cp import pytest @@ -6,8 +6,8 @@ import pytest def test_autograd(): # Validate against micrograd results from Andrej Karpathy # https://github.com/karpathy/micrograd/blob/master/test/test_engine.py - a = variable(-4.0) - b = variable(2.0) + a = value(-4.0) + b = value(2.0) c = a + b d = a * b + b**3 c += c + 1 diff --git a/tests/test_branching_stencils.py b/tests/test_branching_stencils.py index 8751d1f..8e8c244 100644 --- a/tests/test_branching_stencils.py +++ b/tests/test_branching_stencils.py @@ -1,4 +1,4 @@ -from copapy import variable +from copapy import value from copapy.backend import Write, compile_to_dag, add_read_command import copapy as cp import subprocess @@ -20,7 +20,7 @@ def test_compile(): test_vals = [0.0, -1.5, -2.0, -2.5, -3.0] # Function with no passing-on-jump as last instruction: - ret_test = [r for v in test_vals for r in (cp.tan(variable(v)),)] + ret_test = [r for v in test_vals for r in (cp.tan(value(v)),)] out = [Write(r) for r in ret_test] diff --git a/tests/test_comp_timing.py b/tests/test_comp_timing.py index 301851e..b848fb9 100644 --- a/tests/test_comp_timing.py +++ b/tests/test_comp_timing.py @@ -9,9 +9,9 @@ from copapy._compiler import patch_entry, CPConstant, get_aux_func_layout def test_timing_compiler(): - t1 = cp.vector([10, 11]*128) + cp.vector(cp.variable(v) for v in range(256)) + t1 = cp.vector([10, 11]*128) + cp.vector(cp.value(v) for v in range(256)) #t2 = t1.sum() - t3 = cp.vector(cp.variable(1 / (v + 1)) for v in range(256)) + t3 = cp.vector(cp.value(1 / (v + 1)) for v in range(256)) t5 = ((t3 * t1) * 2).magnitude() out = [Write(t5)] diff --git a/tests/test_compile.py b/tests/test_compile.py index ed12b56..9635ff1 100644 --- a/tests/test_compile.py +++ b/tests/test_compile.py @@ -49,10 +49,10 @@ def test_compile(): #ret = function(c1, c2) #ret = [c1 // 3.3 + 5] - t1 = cp.vector([10, 11, 12]) + cp.vector(cp.variable(v) for v in range(3)) + t1 = cp.vector([10, 11, 12]) + cp.vector(cp.value(v) for v in range(3)) t2 = t1.sum() - t3 = cp.vector(cp.variable(1 / (v + 1)) for v in range(3)) + t3 = cp.vector(cp.value(1 / (v + 1)) for v in range(3)) t4 = ((t3 * t1) * 2).sum() t5 = ((t3 * t1) * 2).magnitude() diff --git a/tests/test_compile_aarch64.py b/tests/test_compile_aarch64.py index 552ab77..efc3990 100644 --- a/tests/test_compile_aarch64.py +++ b/tests/test_compile_aarch64.py @@ -43,10 +43,10 @@ def function(c1: NumLike, c2: NumLike) -> tuple[NumLike, ...]: @pytest.mark.runner def test_compile(): - t1 = cp.vector([10, 11, 12]) + cp.vector(cp.variable(v) for v in range(3)) + t1 = cp.vector([10, 11, 12]) + cp.vector(cp.value(v) for v in range(3)) t2 = t1.sum() - t3 = cp.vector(cp.variable(1 / (v + 1)) for v in range(3)) + t3 = cp.vector(cp.value(1 / (v + 1)) for v in range(3)) t4 = ((t3 * t1) * 2).sum() t5 = ((t3 * t1) * 2).magnitude() diff --git a/tests/test_compile_armv7.py b/tests/test_compile_armv7.py index 5b1c6bb..6d52845 100644 --- a/tests/test_compile_armv7.py +++ b/tests/test_compile_armv7.py @@ -43,10 +43,10 @@ def function(c1: NumLike, c2: NumLike) -> tuple[NumLike, ...]: @pytest.mark.runner def test_compile(): - t1 = cp.vector([10, 11, 12]) + cp.vector(cp.variable(v) for v in range(3)) + t1 = cp.vector([10, 11, 12]) + cp.vector(cp.value(v) for v in range(3)) t2 = t1.sum() - t3 = cp.vector(cp.variable(1 / (v + 1)) for v in range(3)) + t3 = cp.vector(cp.value(1 / (v + 1)) for v in range(3)) t4 = ((t3 * t1) * 2).sum() t5 = ((t3 * t1) * 2).magnitude() diff --git a/tests/test_compile_div.py b/tests/test_compile_div.py index 9f1dcc2..87ee766 100644 --- a/tests/test_compile_div.py +++ b/tests/test_compile_div.py @@ -1,4 +1,4 @@ -from copapy import variable, NumLike +from copapy import value, NumLike from copapy.backend import Write, compile_to_dag import copapy import subprocess @@ -22,7 +22,7 @@ def function(c1: NumLike) -> list[NumLike]: @pytest.mark.runner def test_compile(): - c1 = variable(16) + c1 = value(16) ret = function(c1) diff --git a/tests/test_compile_math.py b/tests/test_compile_math.py index 6712535..90385c3 100644 --- a/tests/test_compile_math.py +++ b/tests/test_compile_math.py @@ -1,4 +1,4 @@ -from copapy import variable +from copapy import value from copapy.backend import Write, compile_to_dag, add_read_command import copapy as cp import subprocess @@ -18,7 +18,7 @@ def run_command(command: list[str]) -> str: def test_compile_sqrt(): test_vals = [0.0, 0.0001, 0.1, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0, 5.5, 6.0, 6.28318530718, 100.0, 1000.0, 100000.0] - ret = [r for v in test_vals for r in (cp.sqrt(variable(v)),)] + ret = [r for v in test_vals for r in (cp.sqrt(value(v)),)] out = [Write(r) for r in ret] @@ -52,7 +52,7 @@ def test_compile_sqrt(): def test_compile_log(): test_vals = [0.0, 0.0001, 0.1, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0, 5.5, 6.0, 6.28318530718, 100.0, 1000.0, 100000.0] - ret = [r for v in test_vals for r in (cp.log(variable(v)),)] + ret = [r for v in test_vals for r in (cp.log(value(v)),)] out = [Write(r) for r in ret] @@ -86,7 +86,7 @@ def test_compile_log(): def test_compile_sin(): test_vals = [0.0, 0.0001, 0.1, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0, 5.5, 6.0, 6.28318530718, 100.0, 1000.0, 100000.0] - ret = [r for v in test_vals for r in (cp.sin(variable(v)),)] + ret = [r for v in test_vals for r in (cp.sin(value(v)),)] out = [Write(r) for r in ret] diff --git a/tests/test_coparun_module.py b/tests/test_coparun_module.py index 5985bd1..90b0946 100644 --- a/tests/test_coparun_module.py +++ b/tests/test_coparun_module.py @@ -1,4 +1,4 @@ -from copapy import variable, Target, NumLike +from copapy import value, Target, NumLike import pytest @@ -14,7 +14,7 @@ def function(c1: NumLike) -> list[NumLike]: def test_compile(): - c1 = variable(16) + c1 = value(16) ret = function(c1) diff --git a/tests/test_math.py b/tests/test_math.py index b39396f..534d309 100644 --- a/tests/test_math.py +++ b/tests/test_math.py @@ -1,4 +1,4 @@ -from copapy import variable, Target +from copapy import value, Target import pytest import copapy as cp import math as ma @@ -7,8 +7,8 @@ import warnings def test_fine(): a_i = 9 a_f = 2.5 - c_i = variable(a_i) - c_f = variable(a_f) + c_i = value(a_i) + c_f = value(a_f) # c_b = variable(True) ret_test = (c_f ** 2, @@ -49,7 +49,7 @@ def test_fine(): print('* finished') for test, val2, ref, name in zip(ret_test, re2_test, ret_refe, ('^2', '**-1', 'sqrt_int', 'sqrt_float', 'sin', 'cos', 'tan')): - assert isinstance(test, cp.variable) + assert isinstance(test, cp.value) val = tg.read_value(test) print('+', val, ref, type(val), test.dtype) #for t in (int, float, bool): @@ -63,7 +63,7 @@ def test_trig_precision(): test_vals = [0.0, 0.0001, 0.1, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0, 5.5, 6.0, 6.28318530718, 100.0, 1000.0, 100000.0, -0.0001, -0.1, -0.5, -1.0, -1.5, -2.0, -2.5, -3.0, -3.5, -4.0, -4.5, -5.0, -5.5, -6.0, -6.28318530718, -100.0, -1000.0, -100000.0] - ret_test = [r for v in test_vals for r in (cp.sin(variable(v)), cp.cos(variable(v)), cp.tan(variable(v)))] + ret_test = [r for v in test_vals for r in (cp.sin(value(v)), cp.cos(value(v)), cp.tan(value(v)))] ret_refe = [r for v in test_vals for r in (ma.sin(v), ma.cos(v), ma.tan(v))] tg = Target() @@ -72,7 +72,7 @@ def test_trig_precision(): for i, (v, test, ref) in enumerate(zip(test_vals, ret_test, ret_refe)): func_name = ['sin', 'cos', 'tan'][i % 3] - assert isinstance(test, cp.variable) + assert isinstance(test, cp.value) val = tg.read_value(test) print(f"+ Result of {func_name}: {val}; reference: {ref}") assert val == pytest.approx(ref, abs=1e-3), f"Result of {func_name} for input {test_vals[i // 3]} does not match: {val} and reference: {ref} (value: {v})" # pyright: ignore[reportUnknownMemberType] @@ -85,11 +85,11 @@ def test_arcus_trig_precision(): test_vals = [0.0, 0.01, 0.1, 0.5, 0.7, 0.9, 0.95, -0.01, -0.1, -0.5, -0.7, -0.9, 0.95] - ret_test = [r for v in test_vals for r in (cp.asin(variable(v)), - cp.acos(variable(v)), - cp.atan(variable(v)), - cp.atan2(variable(v), variable(3)), - cp.atan2(variable(v), variable(-3)),)] + ret_test = [r for v in test_vals for r in (cp.asin(value(v)), + cp.acos(value(v)), + cp.atan(value(v)), + cp.atan2(value(v), value(3)), + cp.atan2(value(v), value(-3)),)] ret_refe = [r for v in test_vals for r in (ma.asin(v), ma.acos(v), ma.atan(v), @@ -102,7 +102,7 @@ def test_arcus_trig_precision(): for i, (test, ref) in enumerate(zip(ret_test, ret_refe)): func_name = ['asin', 'acos', 'atan', 'atan2[1]', 'atan2[2]'][i % 5] - assert isinstance(test, cp.variable) + assert isinstance(test, cp.value) val = tg.read_value(test) print(f"+ Result of {func_name}: {val}; reference: {ref}") #assert val == pytest.approx(ref, abs=1e-5), f"Result of {func_name} for input {test_vals[i // 5]} does not match: {val} and reference: {ref}" # pyright: ignore[reportUnknownMemberType] @@ -113,7 +113,7 @@ def test_arcus_trig_precision(): def test_sqrt_precision(): test_vals = [0.0, 0.0001, 0.1, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0, 5.5, 6.0, 6.28318530718, 100.0, 1000.0, 100000.0] - ret_test = [r for v in test_vals for r in (cp.sqrt(variable(v)),)] + ret_test = [r for v in test_vals for r in (cp.sqrt(value(v)),)] ret_refe = [r for v in test_vals for r in (cp.sqrt(v),)] tg = Target() @@ -122,7 +122,7 @@ def test_sqrt_precision(): for i, (test, ref) in enumerate(zip(ret_test, ret_refe)): func_name = 'sqrt' - assert isinstance(test, cp.variable) + assert isinstance(test, cp.value) val = tg.read_value(test) print(f"+ Result of {func_name}: {val}; reference: {ref}") assert val == pytest.approx(ref, rel=1e-5), f"Result of {func_name} for input {test_vals[i]} does not match: {val} and reference: {ref}" # pyright: ignore[reportUnknownMemberType] @@ -135,8 +135,8 @@ def test_log_exp_precision(): test_vals = [0.1, 0.5, 0.9, 0.999, 1.0, 2.5, -0.1, -0.5, -0.9, -0.999, -1.0, 2.5] - ret_test = [r for v in test_vals for r in (cp.log(variable(abs(v))), - cp.exp(variable(v)))] + ret_test = [r for v in test_vals for r in (cp.log(value(abs(v))), + cp.exp(value(v)))] ret_refe = [r for v in test_vals for r in (ma.log(abs(v)), ma.exp(v))] @@ -146,7 +146,7 @@ def test_log_exp_precision(): for i, (test, ref) in enumerate(zip(ret_test, ret_refe)): func_name = ['log', 'exp'][i % 2] - assert isinstance(test, cp.variable) + assert isinstance(test, cp.value) val = tg.read_value(test) print(f"+ Result of {func_name}: {val}; reference: {ref}") assert val == pytest.approx(ref, abs=1e-5), f"Result of {func_name} for input {test_vals[i // 2]} does not match: {val} and reference: {ref}" # pyright: ignore[reportUnknownMemberType] diff --git a/tests/test_matrix.py b/tests/test_matrix.py index 324b481..81ec534 100644 --- a/tests/test_matrix.py +++ b/tests/test_matrix.py @@ -13,11 +13,11 @@ def test_matrix_init(): def test_matrix_with_variables(): """Test matrix initialization with variables""" - m1 = cp.matrix([[cp.variable(1), 2], [3, cp.variable(4)]]) + m1 = cp.matrix([[cp.value(1), 2], [3, cp.value(4)]]) assert m1.rows == 2 assert m1.cols == 2 - assert isinstance(m1[0][0], cp.variable) - assert isinstance(m1[1][1], cp.variable) + assert isinstance(m1[0][0], cp.value) + assert isinstance(m1[1][1], cp.value) def test_matrix_addition(): @@ -201,12 +201,12 @@ def test_matrix_map(): def test_matrix_homogenize(): """Test homogenizing matrix (converting to all variables)""" - m = cp.matrix([[1, cp.variable(2)], [3, 4]]) + m = cp.matrix([[1, cp.value(2)], [3, 4]]) m_homo = m.homogenize() for row in m_homo: for elem in row: - assert isinstance(elem, cp.variable) + assert isinstance(elem, cp.value) def test_identity_matrix(): @@ -254,8 +254,8 @@ def test_diagonal_matrix(): def test_matrix_with_variables_compiled(): """Test matrix operations with variables in compilation""" - m = cp.matrix([[cp.variable(1), 2], [3, cp.variable(4)]]) - v = cp.vector([cp.variable(5), 6]) + m = cp.matrix([[cp.value(1), 2], [3, cp.value(4)]]) + v = cp.vector([cp.value(5), 6]) result = m @ v # result[0] = 1*5 + 2*6 = 17 diff --git a/tests/test_ops.py b/tests/test_ops.py index e2bd62c..33b18d6 100644 --- a/tests/test_ops.py +++ b/tests/test_ops.py @@ -1,4 +1,4 @@ -from copapy import variable, Target, NumLike, iif +from copapy import value, Target, NumLike, iif import pytest import copapy @@ -42,11 +42,11 @@ def iiftests(c1: NumLike) -> list[NumLike]: def test_compile(): - c_i = variable(9) - c_f = variable(1.111) - c_b = variable(True) + c_i = value(9) + c_f = value(1.111) + c_b = value(True) - ret_test = function1(c_i) + function1(c_f) + function2(c_i) + function2(c_f) + function3(c_i) + function4(c_i) + function5(c_b) + [variable(9) % 2] + iiftests(c_i) + iiftests(c_f) + ret_test = function1(c_i) + function1(c_f) + function2(c_i) + function2(c_f) + function3(c_i) + function4(c_i) + function5(c_b) + [value(9) % 2] + iiftests(c_i) + iiftests(c_f) ret_ref = function1(9) + function1(1.111) + function2(9) + function2(1.111) + function3(9) + function4(9) + function5(True) + [9 % 2] + iiftests(9) + iiftests(1.111) tg = Target() @@ -57,7 +57,7 @@ def test_compile(): print('* finished') for test, ref in zip(ret_test, ret_ref): - assert isinstance(test, copapy.variable) + assert isinstance(test, copapy.value) val = tg.read_value(test) print('+', val, ref, test.dtype) for t in (int, float, bool): diff --git a/tests/test_ops_aarch64.py b/tests/test_ops_aarch64.py index 017def3..2b0ffdd 100644 --- a/tests/test_ops_aarch64.py +++ b/tests/test_ops_aarch64.py @@ -1,4 +1,4 @@ -from copapy import NumLike, iif, variable +from copapy import NumLike, iif, value from copapy.backend import Write, compile_to_dag, add_read_command import subprocess from copapy import _binwrite @@ -84,11 +84,11 @@ def iiftests(c1: NumLike) -> list[NumLike]: @pytest.mark.runner def test_compile(): - c_i = variable(9) - c_f = variable(1.111) - c_b = variable(True) + c_i = value(9) + c_f = value(1.111) + c_b = value(True) - ret_test = function1(c_i) + function1(c_f) + function2(c_i) + function2(c_f) + function3(c_i) + function4(c_i) + function5(c_b) + [variable(9) % 2] + iiftests(c_i) + iiftests(c_f) + [cp.asin(c_i/10)] + ret_test = function1(c_i) + function1(c_f) + function2(c_i) + function2(c_f) + function3(c_i) + function4(c_i) + function5(c_b) + [value(9) % 2] + iiftests(c_i) + iiftests(c_f) + [cp.asin(c_i/10)] ret_ref = function1(9) + function1(1.111) + function2(9) + function2(1.111) + function3(9) + function4(9) + function5(True) + [9 % 2] + iiftests(9) + iiftests(1.111) + [cp.asin(9/10)] out = [Write(r) for r in ret_test] @@ -145,7 +145,7 @@ def test_compile(): result_data = parse_results(result) for test, ref in zip(ret_test, ret_ref): - assert isinstance(test, variable) + assert isinstance(test, value) address = variables[test][0] data = result_data[address] if test.dtype == 'int': diff --git a/tests/test_ops_armv7.py b/tests/test_ops_armv7.py index 4941f9b..0c5d552 100644 --- a/tests/test_ops_armv7.py +++ b/tests/test_ops_armv7.py @@ -1,4 +1,4 @@ -from copapy import NumLike, iif, variable +from copapy import NumLike, iif, value from copapy.backend import Write, compile_to_dag, add_read_command import subprocess from copapy import _binwrite @@ -86,11 +86,11 @@ def iiftests(c1: NumLike) -> list[NumLike]: @pytest.mark.runner def test_compile(): - c_i = variable(9) - c_f = variable(1.111) - c_b = variable(True) + c_i = value(9) + c_f = value(1.111) + c_b = value(True) - ret_test = function1(c_i) + function1(c_f) + function2(c_i) + function2(c_f) + function3(c_i) + function4(c_i) + function5(c_b) + [variable(9) % 2] + iiftests(c_i) + iiftests(c_f) + [cp.asin(c_i/10)] + ret_test = function1(c_i) + function1(c_f) + function2(c_i) + function2(c_f) + function3(c_i) + function4(c_i) + function5(c_b) + [value(9) % 2] + iiftests(c_i) + iiftests(c_f) + [cp.asin(c_i/10)] ret_ref = function1(9) + function1(1.111) + function2(9) + function2(1.111) + function3(9) + function4(9) + function5(True) + [9 % 2] + iiftests(9) + iiftests(1.111) + [cp.asin(9/10)] #ret_test = (c_i * 100 // 5, c_f * 10 // 5) @@ -147,7 +147,7 @@ def test_compile(): result_data = parse_results(result) for test, ref in zip(ret_test, ret_ref): - assert isinstance(test, variable) + assert isinstance(test, value) address = variables[test][0] data = result_data[address] if test.dtype == 'int': diff --git a/tests/test_ops_x86.py b/tests/test_ops_x86.py index 069f341..6ea427d 100644 --- a/tests/test_ops_x86.py +++ b/tests/test_ops_x86.py @@ -1,4 +1,4 @@ -from copapy import NumLike, iif, variable +from copapy import NumLike, iif, value from copapy.backend import Write, compile_to_dag, add_read_command import subprocess from copapy import _binwrite @@ -77,7 +77,7 @@ def test_compile(): #t4 = ((t3 * t1) * 2).sum() #t5 = ((t3 * t1) * 2).magnitude() - c_i = variable(9) + c_i = value(9) #c_f = variable(1.111) #c_b = variable(True) @@ -87,7 +87,7 @@ def test_compile(): #ret_test = [cp.sin(c_i), cp.asin(variable(0.0))] #ret_ref = [cp.sin(9), cp.asin(0.0)] - ret_test: list[variable[float]] = [] + ret_test: list[value[float]] = [] ret_ref: list[float] = [] #sval = variable(8.0) #tval = 8.0 @@ -155,7 +155,7 @@ def test_compile(): result_data = parse_results(result) for test, ref in zip(ret_test, ret_ref): - assert isinstance(test, variable) + assert isinstance(test, value) address = variables[test][0] data = result_data[address] if test.dtype == 'int': diff --git a/tests/test_prog_flow.py b/tests/test_prog_flow.py index bd796c8..5c4563f 100644 --- a/tests/test_prog_flow.py +++ b/tests/test_prog_flow.py @@ -1,11 +1,11 @@ -from copapy import variable, Target, iif +from copapy import value, Target, iif import pytest import copapy def test_compile(): - c_i = variable(9) - c_f = variable(2.5) + c_i = value(9) + c_f = value(2.5) # c_b = variable(True) ret_test = (iif(c_f > 5, c_f, -1), iif(c_i > 5, c_f, 8.8), iif(c_i > 2, c_i, 1)) @@ -19,7 +19,7 @@ def test_compile(): print('* finished') for test, ref in zip(ret_test, ret_ref): - assert isinstance(test, copapy.variable) + assert isinstance(test, copapy.value) val = tg.read_value(test) print('+', val, ref, type(val), test.dtype) #for t in (int, float, bool): diff --git a/tests/test_readme_example.py b/tests/test_readme_example.py index 201d4fa..354c563 100644 --- a/tests/test_readme_example.py +++ b/tests/test_readme_example.py @@ -3,8 +3,8 @@ import pytest def test_readme_example(): # Define variables - a = cp.variable(0.25) - b = cp.variable(0.87) + a = cp.value(0.25) + b = cp.value(0.87) # Define computations c = a + b * 2.0 diff --git a/tests/test_rev_kinematics.py b/tests/test_rev_kinematics.py index abf04ab..9072490 100644 --- a/tests/test_rev_kinematics.py +++ b/tests/test_rev_kinematics.py @@ -10,7 +10,7 @@ target = cp.vector([0.7, 0.7]) alpha = 0.1 -def forward_kinematics(theta1: cp.variable[float] | float, theta2: cp.variable[float] | float) -> tuple[cp.vector[float], cp.vector[float]]: +def forward_kinematics(theta1: cp.value[float] | float, theta2: cp.value[float] | float) -> tuple[cp.vector[float], cp.vector[float]]: """Return positions of joint and end-effector.""" joint = cp.vector([l1 * cp.cos(theta1), l1 * cp.sin(theta1)]) end_effector = joint + cp.vector([l2 * cp.cos(theta1 + theta2), @@ -20,7 +20,7 @@ def forward_kinematics(theta1: cp.variable[float] | float, theta2: cp.variable[f def test_two_arms(): target_vec = cp.vector(target) - theta = cp.vector([cp.variable(0.0), cp.variable(0.0)]) + theta = cp.vector([cp.value(0.0), cp.value(0.0)]) joint = cp.vector([0.0, 0.0]) effector = cp.vector([0.0, 0.0]) diff --git a/tests/test_vector.py b/tests/test_vector.py index c893f6b..b7ce95c 100644 --- a/tests/test_vector.py +++ b/tests/test_vector.py @@ -5,38 +5,38 @@ from copapy import filters def test_vectors_init(): tt1 = cp.vector(range(3)) + cp.vector([1.1, 2.2, 3.3]) - tt2 = cp.vector([1.1, 2, cp.variable(5)]) + cp.vector(range(3)) + tt2 = cp.vector([1.1, 2, cp.value(5)]) + cp.vector(range(3)) tt3 = (cp.vector(range(3)) + 5.6) - tt4 = cp.vector([1.1, 2, 3]) + cp.vector(cp.variable(v) for v in range(3)) + tt4 = cp.vector([1.1, 2, 3]) + cp.vector(cp.value(v) for v in range(3)) tt5 = cp.vector([1, 2, 3]).dot(tt4) print(tt1, tt2, tt3, tt4, tt5) def test_compiled_vectors(): - t1 = cp.vector([10, 11, 12]) + cp.vector(cp.variable(v) for v in range(3)) + t1 = cp.vector([10, 11, 12]) + cp.vector(cp.value(v) for v in range(3)) t2 = t1.sum() - t3 = cp.vector(cp.variable(1 / (v + 1)) for v in range(3)) + t3 = cp.vector(cp.value(1 / (v + 1)) for v in range(3)) t4 = ((t3 * t1) * 2).sum() t5 = ((t3 * t1) * 2).magnitude() - t6 = cp.angle_between(cp.vector([cp.variable(5.0), 0.0, 0.0]), cp.vector([5.0, 5.0, 0.0])) + t6 = cp.angle_between(cp.vector([cp.value(5.0), 0.0, 0.0]), cp.vector([5.0, 5.0, 0.0])) tg = cp.Target() tg.compile(t2, t4, t5, t6) tg.run() - assert isinstance(t2, cp.variable) + assert isinstance(t2, cp.value) assert tg.read_value(t2) == 10 + 11 + 12 + 0 + 1 + 2 - assert isinstance(t4, cp.variable) + assert isinstance(t4, cp.value) assert tg.read_value(t4) == pytest.approx(((10/1*2) + (12/2*2) + (14/3*2)), 0.001) # pyright: ignore[reportUnknownMemberType] - assert isinstance(t5, cp.variable) + assert isinstance(t5, cp.value) assert tg.read_value(t5) == pytest.approx(((10/1*2)**2 + (12/2*2)**2 + (14/3*2)**2) ** 0.5, 0.001) # pyright: ignore[reportUnknownMemberType] - assert isinstance(t6, cp.variable) + assert isinstance(t6, cp.value) assert tg.read_value(t6) == pytest.approx(math.pi / 4, 0.001), tg.read_value(t6) # pyright: ignore[reportUnknownMemberType] @@ -97,7 +97,7 @@ def test_non_compiled_vector_operations(): def test_sort_vector(): vlist = [50, 21, 20, 10, 22, 1, 80, 70, 90] - t1 = cp.vector(cp.variable(v) for v in vlist) + t1 = cp.vector(cp.value(v) for v in vlist) #t1 = cp.vector(v for v in vlist) t2 = filters.median(t1) diff --git a/tools/make_example.py b/tools/make_example.py index 672f0e1..2f16334 100644 --- a/tools/make_example.py +++ b/tools/make_example.py @@ -1,4 +1,4 @@ -from copapy import variable +from copapy import value from copapy.backend import Write, compile_to_dag, stencil_db_from_package from copapy._binwrite import Command import copapy as cp @@ -6,7 +6,7 @@ import copapy as cp def compile_to_x86_64() -> None: """Test compilation of a simple program for x86_64.""" - c1 = variable(9.0) + c1 = value(9.0) #ret = [c1 / 4, c1 / -4, c1 // 4, c1 // -4, (c1 * -1) // 4] ret = [c1 // 3.3 + 5] @@ -29,14 +29,14 @@ def compile_to_x86_64() -> None: def compile_to_x86() -> None: """Test compilation of a simple program for x86 32 bit.""" - c1 = variable(9.0) + c1 = value(9.0) #ret = [c1 / 4, c1 / -4, c1 // 4, c1 // -4, (c1 * -1) // 4] ret = [c1 // 3.3 + 5] #ret = [cp.sqrt(c1)] #c2 = cp._math.get_42() #ret = [c2] - ret = [cp.sin(variable(2.5))] + ret = [cp.sin(value(2.5))] out = [Write(r) for r in ret] @@ -53,7 +53,7 @@ def compile_to_x86() -> None: def compile_to_aarch64() -> None: """Test compilation of a simple program for arm64.""" - c1 = variable(9.0) + c1 = value(9.0) #ret = [c1 / 4, c1 / -4, c1 // 4, c1 // -4, (c1 * -1) // 4] #ret = [cp.sin(c1), cp.sqrt(c1) + 5] From bf1c3468b38142e0649aba19bf8818dc4d4e36c7 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Sat, 6 Dec 2025 18:09:41 +0100 Subject: [PATCH 34/39] readme updated --- README.md | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 2cdb1e0..d7cd39c 100644 --- a/README.md +++ b/README.md @@ -9,11 +9,11 @@ Embedded systems comes with a variety of CPU architectures. The **copy and patch The summarized main features are: - Fast to write & easy to read -- Memory and type safety, minimal set of runtime errors [^1] +- Memory and type safety, minimal set of runtime errors - deterministic execution - Auto grad for efficient realtime optimizations -- Optimized machine code for the target architectures x68_64, Aarch64 and ARMv7 [^3] -- Very portable to new architectures [^2] +- Optimized machine code for the target architectures x68_64, Aarch64 and ARMv7 +- Very portable to new architectures - Small python package, minimal dependencies, no cross compile toolchain required ## Current state @@ -24,21 +24,23 @@ Currently worked on: - For targeting Crossover‑MCUs, support for Thumb instructions required by ARM*-M is on the way. - Constant-regrouping for symbolic optimization of the computation graph. -## Getting started & example +## Install To install copapy, you can use pip. Precompiled wheels are available for Linux (x86_64, Aarch64 and ARMv7), Windows (x86_64) and Mac OS (x86_64, Aarch64): ```bash pip install copapy ``` +## Examples +### Basic example A very simple example program using copapy can look like this: ```python import copapy as cp # Define variables -a = cp.variable(0.25) -b = cp.variable(0.87) +a = cp.value(0.25) +b = cp.value(0.87) # Define computations c = a + b * 2.0 @@ -56,6 +58,7 @@ print("Result d:", tg.read_value(d)) print("Result e:", tg.read_value(e)) ``` +### Inverse kinematics An other example using autograd in copapy. Here for for implementing gradient descent to solve a reverse kinematic problem for a two joint 2D arm: @@ -80,15 +83,14 @@ def forward_kinematics(theta1, theta2): return joint, end_effector # Start values -theta = cp.vector([cp.variable(0.0), cp.variable(0.0)]) +theta = cp.vector([cp.value(0.0), cp.value(0.0)]) # Iterative inverse kinematic for _ in range(48): joint, effector = forward_kinematics(theta[0], theta[1]) error = ((target - effector) ** 2).sum() - grad_vec = cp.grad(error, theta) - theta -= alpha * grad_vec + theta -= alpha * cp.grad(error, theta) tg = cp.Target() tg.compile(error, theta, joint) From 2cb549929951be4ea3a26e802485850be542f91a Mon Sep 17 00:00:00 2001 From: Nicolas Date: Sat, 6 Dec 2025 18:11:42 +0100 Subject: [PATCH 35/39] sphinx based docs added --- .gitignore | 2 + docs/Makefile | 20 +++++++ docs/make.bat | 35 ++++++++++++ docs/source/conf.py | 37 +++++++++++++ docs/source/generate_class_list.py | 86 ++++++++++++++++++++++++++++++ docs/source/index.md | 9 ++++ docs/source/repo.md | 3 ++ pyproject.toml | 6 +++ 8 files changed, 198 insertions(+) create mode 100644 docs/Makefile create mode 100644 docs/make.bat create mode 100644 docs/source/conf.py create mode 100644 docs/source/generate_class_list.py create mode 100644 docs/source/index.md create mode 100644 docs/source/repo.md diff --git a/.gitignore b/.gitignore index f6a7942..bdd5a7a 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,5 @@ build/* /src/*.pyd vc140.pdb benchmark_results* +docs/build +docs/source/api \ No newline at end of file diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..d0c3cbf --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = source +BUILDDIR = build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..dc1312a --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=source +set BUILDDIR=build + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.https://www.sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "" goto help + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 0000000..55a33b6 --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,37 @@ +# Configuration file for the Sphinx documentation builder. +# +# For the full list of built-in configuration values, see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Project information ----------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information + +import os +import sys +sys.path.insert(0, os.path.abspath("../src/")) + +project = 'copapy' +copyright = '2025, Nicolas Kruse' +author = 'Nicolas Kruse' + +# -- General configuration --------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration + +extensions = ["sphinx.ext.autodoc", "sphinx.ext.napoleon", "sphinx_autodoc_typehints", "myst_parser"] + +templates_path = ['_templates'] +exclude_patterns = [] + +# -- Options for HTML output ------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output + +# html_theme = 'alabaster' +html_theme = 'pydata_sphinx_theme' +html_static_path = ['_static'] +html_theme_options = { + "secondary_sidebar_items": ["page-toc"], + "footer_start": ["copyright"] +} +html_theme_options["footer_end"] = [] + +autodoc_inherit_docstrings = True diff --git a/docs/source/generate_class_list.py b/docs/source/generate_class_list.py new file mode 100644 index 0000000..e69df0f --- /dev/null +++ b/docs/source/generate_class_list.py @@ -0,0 +1,86 @@ +# This script generates the source md-files for all classes and functions for the docs + +import importlib +import inspect +import fnmatch +from io import TextIOWrapper +import os + + +def write_manual(f: TextIOWrapper, doc_files: list[str], title: str) -> None: + write_dochtree(f, title, doc_files) + + +def write_classes(f: TextIOWrapper, patterns: list[str], module_name: str, title: str, description: str = '', exclude: list[str] = []) -> None: + """Write the classes to the file.""" + module = importlib.import_module(module_name) + + classes = [ + name for name, obj in inspect.getmembers(module, inspect.isclass) + if (any(fnmatch.fnmatch(name, pat) for pat in patterns if pat not in exclude) and + obj.__doc__ and '(Automatic generated stub)' not in obj.__doc__) + ] + + if description: + f.write(f'{description}\n\n') + + write_dochtree(f, title, classes) + + for cls in classes: + with open(f'docs/source/api/{cls}.md', 'w') as f2: + f2.write(f'# {module_name}.{cls}\n') + f2.write('```{eval-rst}\n') + f2.write(f'.. autoclass:: {module_name}.{cls}\n') + f2.write(' :members:\n') + f2.write(' :undoc-members:\n') + f2.write(' :show-inheritance:\n') + f2.write(' :inherited-members:\n') + f2.write('```\n\n') + + +def write_functions(f: TextIOWrapper, patterns: list[str], module_name: str, title: str, description: str = '', exclude: list[str] = []) -> None: + """Write the classes to the file.""" + module = importlib.import_module(module_name) + + functions = [ + name for name, _ in inspect.getmembers(module, inspect.isfunction) + if (any(fnmatch.fnmatch(name, pat) for pat in patterns if pat not in exclude)) + ] + + if description: + f.write(f'{description}\n\n') + + write_dochtree(f, title, functions) + + for func in functions: + if not func.startswith('_'): + with open(f'docs/source/api/{func}.md', 'w') as f2: + f2.write(f'# {module_name}.{func}\n') + f2.write('```{eval-rst}\n') + f2.write(f'.. autofunction:: {module_name}.{func}\n') + f2.write('```\n\n') + + +def write_dochtree(f: TextIOWrapper, title: str, items: list[str]): + f.write('```{toctree}\n') + f.write(':maxdepth: 1\n') + f.write(f':caption: {title}:\n') + #f.write(':hidden:\n') + for text in items: + if not text.startswith('_'): + f.write(f"{text}\n") + f.write('```\n\n') + + +if __name__ == "__main__": + # Ensure the output directory exists + os.makedirs('docs/source/api', exist_ok=True) + + with open('docs/source/api/index.md', 'w') as f: + f.write('# Classes and functions\n\n') + + write_classes(f, ['*'], 'copapy', title='Classes') + + write_functions(f, ['*'], 'copapy', title='Functions') + + #write_manual(f, ['../ndfloat', '../floatarray'], title='Types') diff --git a/docs/source/index.md b/docs/source/index.md new file mode 100644 index 0000000..b65f4c4 --- /dev/null +++ b/docs/source/index.md @@ -0,0 +1,9 @@ +```{toctree} +:maxdepth: 1 +:hidden: +api/index +repo +``` + +```{include} ../../README.md +``` \ No newline at end of file diff --git a/docs/source/repo.md b/docs/source/repo.md new file mode 100644 index 0000000..110e578 --- /dev/null +++ b/docs/source/repo.md @@ -0,0 +1,3 @@ +# Code repository + +Code repository is on GitHub: [github.com/Nonannet/copapy](https://github.com/Nonannet/copapy). \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 6108eda..0900c1a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,6 +36,12 @@ dev = [ "mypy", "pytest" ] +doc_build = [ + "sphinx", + "pydata_sphinx_theme", + "sphinx-autodoc-typehints", + "myst-parser" +] [tool.mypy] files = ["src", "tools", "stencils"] From dd90b61c7bd5b5bbd45f46a84542191e6c4b9b8d Mon Sep 17 00:00:00 2001 From: Nicolas Kruse Date: Sat, 6 Dec 2025 23:24:38 +0100 Subject: [PATCH 36/39] ci updated --- .github/workflows/build_wheels.yml | 32 ++++++++--------- .github/workflows/ci.yml | 58 ++++++------------------------ tools/create_asm.sh | 23 ++++++++++++ tools/crosscompile.sh | 7 ---- 4 files changed, 49 insertions(+), 71 deletions(-) create mode 100644 tools/create_asm.sh diff --git a/.github/workflows/build_wheels.yml b/.github/workflows/build_wheels.yml index 705b220..1b9bafa 100644 --- a/.github/workflows/build_wheels.yml +++ b/.github/workflows/build_wheels.yml @@ -65,21 +65,21 @@ jobs: name: wheels-${{ matrix.os }} path: wheelhouse/*.whl -# publish: -# if: contains(github.ref, '-beta') == false -# needs: [build_wheels] -# runs-on: ubuntu-latest -# steps: -# - name: Install Twine -# run: pip install twine + publish: + if: contains(github.ref, '-beta') == false + needs: [build_wheels] + runs-on: ubuntu-latest + steps: + - name: Install Twine + run: pip install twine -# - uses: actions/download-artifact@v4 -# with: -# path: wheelhouse + - uses: actions/download-artifact@v4 + with: + path: wheelhouse -# - name: Publish to PyPI -# env: -# TWINE_USERNAME: __token__ -# TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} -# -# run: python -m twine upload wheelhouse/* \ No newline at end of file + - name: Publish to PyPI + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} + + run: python -m twine upload wheelhouse/* diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 41a47f1..9a56d12 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,11 +30,6 @@ jobs: name: musl-object-files path: /object_files/musl_objects_*.*o - - uses: actions/upload-artifact@v4 - with: - name: cross-runner - path: build/runner/coparun-* - build-ubuntu: needs: [build_stencils] runs-on: ubuntu-latest @@ -52,11 +47,6 @@ jobs: name: stencil-object-files path: src/copapy/obj - #- uses: actions/download-artifact@v4 - # with: - # name: cross-runner - # path: build/runner - - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: @@ -65,46 +55,23 @@ jobs: - name: Install Python dependencies run: python -m pip install -e .[dev] - #- name: Install ARM binutils and qemu - # if: strategy.job-index == 0 - # run: | - # echo "set man-db/auto-update false" | sudo debconf-communicate - # sudo dpkg-reconfigure man-db - # sudo apt-get update - # sudo apt-get install --no-install-recommends --no-install-suggests binutils-aarch64-linux-gnu qemu-user gcc-aarch64-linux-gnu libc6-dev-arm64-cross - - name: Compile coparun run: | mkdir -p build/runner gcc -O3 -DENABLE_BASIC_LOGGING -o build/runner/coparun src/coparun/runmem.c src/coparun/coparun.c src/coparun/mem_man.c - # aarch64-linux-gnu-gcc -O3 -static -DENABLE_BASIC_LOGGING src/coparun/runmem.c src/coparun/coparun.c src/coparun/mem_man.c -o build/runner/coparun-aarch64 - name: Generate debug asm files if: strategy.job-index == 0 run: | set -e set -v - python tools/make_example.py + bash tools/create_asm.sh - echo "- Patch code..." - build/runner/coparun build/runner/test.copapy build/runner/test.copapy.bin - #qemu-aarch64 build/runner/coparun-aarch64 build/runner/test-arm64.copapy build/runner/test-arm64.copapy.bin + echo '

example

' >> $GITHUB_STEP_SUMMARY + python tools/clean_asm.py build/asm/example.asm >> $GITHUB_STEP_SUMMARY - objdump -D -b binary -m i386:x86-64 --adjust-vma=0x1000 build/runner/test.copapy.bin > build/runner/test.copapy.asm - echo '

test.copapy.asm

' >> $GITHUB_STEP_SUMMARY - python tools/clean_asm.py build/runner/test.copapy.asm >> $GITHUB_STEP_SUMMARY - - #aarch64-linux-gnu-objdump -D -b binary -m aarch64 --adjust-vma=0x1000 build/runner/test-arm64.copapy.bin > build/runner/test-arm64.copapy.asm - #echo '

test-arm64.copapy.asm

' >> $GITHUB_STEP_SUMMARY - #python tools/clean_asm.py build/runner/test-arm64.copapy.asm >> $GITHUB_STEP_SUMMARY - - objdump -d -x src/copapy/obj/stencils_x86_64_O3.o > build/runner/stencils_x86_64_O3.asm - echo '

stencils_x86_64_O3.asm

' >> $GITHUB_STEP_SUMMARY - python tools/clean_asm.py build/runner/stencils_x86_64_O3.asm >> $GITHUB_STEP_SUMMARY - - #aarch64-linux-gnu-objdump -d -x src/copapy/obj/stencils_arm64_O3.o > build/runner/stencils_arm64_O3.asm - #echo '

stencils_arm64_O3.asm

' >> $GITHUB_STEP_SUMMARY - #python tools/clean_asm.py build/runner/stencils_arm64_O3.asm >> $GITHUB_STEP_SUMMARY + echo '

stencils_x86_64_O3.o

' >> $GITHUB_STEP_SUMMARY + python tools/clean_asm.py build/asm/stencils.asm >> $GITHUB_STEP_SUMMARY - name: Run tests with pytest run: pytest @@ -142,7 +109,8 @@ jobs: mkdir -p build/runner && \ gcc -O3 -DENABLE_LOGGING -o build/runner/coparun src/coparun/runmem.c \ src/coparun/coparun.c src/coparun/mem_man.c && \ - pytest" + pytest && \ + bash tools/create_asm.sh" - uses: actions/upload-artifact@v4 with: @@ -170,7 +138,8 @@ jobs: mkdir -p build/runner && \ gcc -O3 -DENABLE_LOGGING -o build/runner/coparun src/coparun/runmem.c \ src/coparun/coparun.c src/coparun/mem_man.c && \ - pytest" + pytest && \ + bash tools/create_asm.sh" - uses: actions/upload-artifact@v4 with: @@ -219,12 +188,6 @@ jobs: - name: Run tests with pytest run: pytest - - name: Type checking with mypy - run: mypy - - #- name: Lint code with flake8 - # run: flake8 - - uses: actions/upload-artifact@v4 if: strategy.job-index == 0 with: @@ -234,7 +197,7 @@ jobs: release-stencils: needs: [build_stencils, build-ubuntu, build-windows, build-arm64, build-armv7] runs-on: ubuntu-latest - if: github.event_name == 'push' + if: github.ref == 'refs/heads/main' && github.event_name == 'push' permissions: contents: write steps: @@ -262,7 +225,6 @@ jobs: mkdir -p release cp tmp/stencil-object-files/* release/ cp tmp/musl-object-files/* release/ - cp tmp/cross-runner/coparun-* release/ cp tmp/runner-linux/coparun release/ cp tmp/runner-linux-arm64/coparun release/coparun-aarch64 cp tmp/runner-linux-armv7/coparun release/coparun-armv7 diff --git a/tools/create_asm.sh b/tools/create_asm.sh new file mode 100644 index 0000000..3cbbd81 --- /dev/null +++ b/tools/create_asm.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +set -e +set -v + +mkdir -p build/runner + +arch=$(python3 -c "import copapy; print(copapy._stencils.detect_process_arch())") + +# Disassemble stencil object file +objdump -d -x src/copapy/obj/stencils_${arch}_O3.o > build/runner/stencils.asm + +# Create example code disassembly +python3 tools/make_example.py +build/runner/coparun build/runner/test.copapy build/runner/test.copapy.bin + +if [ $(arch) = 'x86-64' ]; then + arch="i386:x86-64" +fi + +objdump -D -b binary -m $arch --adjust-vma=0x10000 build/runner/test.copapy.bin > build/runner/example.asm + +rm build/runner/test.copapy.bin diff --git a/tools/crosscompile.sh b/tools/crosscompile.sh index 67a118c..32efa2c 100644 --- a/tools/crosscompile.sh +++ b/tools/crosscompile.sh @@ -58,10 +58,3 @@ arm-none-eabi-ld -r $STMP /object_files/musl_objects_armv7.o $LIBGCC -o $DEST/st # RISCV 64 Bit #riscv64-linux-gnu-gcc-13 $FLAGS -$OPT -c $SRC -o $DEST/stencils_riscv64_$OPT.o - - -# -------------- Cross compile runner -------------- -mkdir -p build/runner - -# Aarch64 -aarch64-linux-gnu-gcc-13 -static -O3 -DENABLE_LOGGING -o build/runner/coparun-aarch64 src/coparun/runmem.c src/coparun/coparun.c src/coparun/mem_man.c From deb0b88a5491a732223afa6ed5d998d82271c154 Mon Sep 17 00:00:00 2001 From: Nicolas Kruse Date: Sat, 6 Dec 2025 23:25:15 +0100 Subject: [PATCH 37/39] extract_sections script for docs added --- docs/source/extract_section.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 docs/source/extract_section.py diff --git a/docs/source/extract_section.py b/docs/source/extract_section.py new file mode 100644 index 0000000..1162405 --- /dev/null +++ b/docs/source/extract_section.py @@ -0,0 +1,29 @@ +import re + +def extract_sections(md_text: str) -> dict[str, str]: + """ + Extracts sections based on headings (#...). + Returns {heading_text: section_content} + Works for simple Markdown, not fully strict. + """ + + # regex captures: heading marks (###...), heading text, and the following content + pattern = re.compile( + r'^(#{1,6})\s+(.*?)\s*$' # heading level + heading text + r'(.*?)' # section content (lazy) + r'(?=^#{1,6}\s+|\Z)', # stop at next heading or end of file + re.MULTILINE | re.DOTALL + ) + + sections: dict[str, str] = {} + for _, title, content in pattern.findall(md_text): + sections[title] = content.strip() + + return sections + +if __name__ == '__main__': + with open('README.md', 'rt') as f: + readme = extract_sections(f.read()) + + with open('docs/source/start.md', 'wt') as f: + f.write('\n'.join(readme[s] for s in ['Copapy', 'Current state'])) \ No newline at end of file From d041312315abfdf20fc36c8889c8156e2d4000cb Mon Sep 17 00:00:00 2001 From: Nicolas Date: Sun, 7 Dec 2025 12:09:29 +0100 Subject: [PATCH 38/39] architecture name translation fixed in create_asm.sh --- tools/create_asm.sh | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/tools/create_asm.sh b/tools/create_asm.sh index 3cbbd81..b939fb8 100644 --- a/tools/create_asm.sh +++ b/tools/create_asm.sh @@ -14,10 +14,20 @@ objdump -d -x src/copapy/obj/stencils_${arch}_O3.o > build/runner/stencils.asm python3 tools/make_example.py build/runner/coparun build/runner/test.copapy build/runner/test.copapy.bin -if [ $(arch) = 'x86-64' ]; then +if [ $(arch) = 'x86_64' ]; then arch="i386:x86-64" +elif [ $(arch) = 'x86' ]; then + arch="i386" +elif [ $(arch) = 'arm64' ]; then + arch="aarch64" +elif [ $(arch) = 'armv6' ]; then + arch="arm" +elif [ $(arch) = 'armv7' ]; then + arch="arm" fi +echo "Archtitecture: '$arch'" + objdump -D -b binary -m $arch --adjust-vma=0x10000 build/runner/test.copapy.bin > build/runner/example.asm rm build/runner/test.copapy.bin From 9b78947202d84d17db0747858422bc9ee44820e1 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Sun, 7 Dec 2025 12:38:55 +0100 Subject: [PATCH 39/39] ci: fixed make_example.py and path names --- .github/workflows/ci.yml | 4 +-- tools/clean_asm.py | 2 +- tools/create_asm.sh | 28 +++++++++---------- tools/make_example.py | 59 ++++------------------------------------ 4 files changed, 22 insertions(+), 71 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9a56d12..0f7177b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -68,10 +68,10 @@ jobs: bash tools/create_asm.sh echo '

example

' >> $GITHUB_STEP_SUMMARY - python tools/clean_asm.py build/asm/example.asm >> $GITHUB_STEP_SUMMARY + python tools/clean_asm.py build/runner/example.asm >> $GITHUB_STEP_SUMMARY echo '

stencils_x86_64_O3.o

' >> $GITHUB_STEP_SUMMARY - python tools/clean_asm.py build/asm/stencils.asm >> $GITHUB_STEP_SUMMARY + python tools/clean_asm.py build/runner/stencils.asm >> $GITHUB_STEP_SUMMARY - name: Run tests with pytest run: pytest diff --git a/tools/clean_asm.py b/tools/clean_asm.py index ae39f05..b14193b 100644 --- a/tools/clean_asm.py +++ b/tools/clean_asm.py @@ -28,7 +28,7 @@ def main() -> None: if outp_flag: print(line + '
') - if "Disassembly of section .text:" in line: + if "Disassembly of section .text" in line: outp_flag = True print('') diff --git a/tools/create_asm.sh b/tools/create_asm.sh index b939fb8..ca6e220 100644 --- a/tools/create_asm.sh +++ b/tools/create_asm.sh @@ -5,29 +5,29 @@ set -v mkdir -p build/runner -arch=$(python3 -c "import copapy; print(copapy._stencils.detect_process_arch())") +cparch=$(python3 -c "import copapy; print(copapy._stencils.detect_process_arch())") # Disassemble stencil object file -objdump -d -x src/copapy/obj/stencils_${arch}_O3.o > build/runner/stencils.asm +objdump -d -x src/copapy/obj/stencils_${cparch}_O3.o > build/runner/stencils.asm # Create example code disassembly python3 tools/make_example.py build/runner/coparun build/runner/test.copapy build/runner/test.copapy.bin -if [ $(arch) = 'x86_64' ]; then - arch="i386:x86-64" -elif [ $(arch) = 'x86' ]; then - arch="i386" -elif [ $(arch) = 'arm64' ]; then - arch="aarch64" -elif [ $(arch) = 'armv6' ]; then - arch="arm" -elif [ $(arch) = 'armv7' ]; then - arch="arm" +if [ "$cparch" = 'x86_64' ]; then + cparch="i386:x86-64" +elif [ "$cparch" = 'x86' ]; then + cparch="i386" +elif [ "$cparch" = 'arm64' ]; then + cparch="aarch64" +elif [ "$cparch" = 'armv6' ]; then + cparch="arm" +elif [ "$cparch" = 'armv7' ]; then + cparch="arm" fi -echo "Archtitecture: '$arch'" +echo "Archtitecture: '$cparch'" -objdump -D -b binary -m $arch --adjust-vma=0x10000 build/runner/test.copapy.bin > build/runner/example.asm +objdump -D -b binary -m $cparch --adjust-vma=0x10000 build/runner/test.copapy.bin > build/runner/example.asm rm build/runner/test.copapy.bin diff --git a/tools/make_example.py b/tools/make_example.py index 2f16334..6d9cebc 100644 --- a/tools/make_example.py +++ b/tools/make_example.py @@ -4,7 +4,7 @@ from copapy._binwrite import Command import copapy as cp -def compile_to_x86_64() -> None: +def compile_example(arch: str = 'native') -> None: """Test compilation of a simple program for x86_64.""" c1 = value(9.0) @@ -16,65 +16,16 @@ def compile_to_x86_64() -> None: out = [Write(r) for r in ret] - sdb = stencil_db_from_package('x86_64') + sdb = stencil_db_from_package(arch) dw, _ = compile_to_dag(out, sdb) dw.write_com(Command.DUMP_CODE) - print('* Data to runner:') - dw.print() + #print('* Data to runner:') + #dw.print() dw.to_file('build/runner/test.copapy') -def compile_to_x86() -> None: - """Test compilation of a simple program for x86 32 bit.""" - c1 = value(9.0) - - #ret = [c1 / 4, c1 / -4, c1 // 4, c1 // -4, (c1 * -1) // 4] - ret = [c1 // 3.3 + 5] - #ret = [cp.sqrt(c1)] - #c2 = cp._math.get_42() - #ret = [c2] - ret = [cp.sin(value(2.5))] - - out = [Write(r) for r in ret] - - sdb = stencil_db_from_package('x86') - dw, _ = compile_to_dag(out, sdb) - - dw.write_com(Command.DUMP_CODE) - - print('* Data to runner:') - dw.print() - - dw.to_file('build/runner/test-x86.copapy') - - -def compile_to_aarch64() -> None: - """Test compilation of a simple program for arm64.""" - c1 = value(9.0) - - #ret = [c1 / 4, c1 / -4, c1 // 4, c1 // -4, (c1 * -1) // 4] - #ret = [cp.sin(c1), cp.sqrt(c1) + 5] - ret = [c1 // 3.3 + 5] - #c2 = cp._math.get_42() - #ret = [c2] - - out = [Write(r) for r in ret] - - sdb = stencil_db_from_package('arm64') - dw, _ = compile_to_dag(out, sdb) - - dw.write_com(Command.DUMP_CODE) - - print('* Data to runner:') - dw.print() - - dw.to_file('build/runner/test-arm64.copapy') - - if __name__ == "__main__": - compile_to_x86_64() - compile_to_x86() - compile_to_aarch64() + compile_example()