aether-dsc 0.2.6__tar.gz → 0.2.7__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {aether_dsc-0.2.6/aether_dsc.egg-info → aether_dsc-0.2.7}/PKG-INFO +12 -1
- {aether_dsc-0.2.6 → aether_dsc-0.2.7}/README.md +11 -0
- {aether_dsc-0.2.6 → aether_dsc-0.2.7}/aether_dsc/__init__.py +1 -1
- aether_dsc-0.2.7/aether_dsc/c_core.py +421 -0
- {aether_dsc-0.2.6 → aether_dsc-0.2.7/aether_dsc.egg-info}/PKG-INFO +12 -1
- {aether_dsc-0.2.6 → aether_dsc-0.2.7}/aether_dsc.egg-info/SOURCES.txt +2 -0
- {aether_dsc-0.2.6 → aether_dsc-0.2.7}/pyproject.toml +1 -1
- aether_dsc-0.2.7/tests/test_c_core.py +221 -0
- {aether_dsc-0.2.6 → aether_dsc-0.2.7}/LICENSE +0 -0
- {aether_dsc-0.2.6 → aether_dsc-0.2.7}/NOTICE +0 -0
- {aether_dsc-0.2.6 → aether_dsc-0.2.7}/aether_dsc/_core.py +0 -0
- {aether_dsc-0.2.6 → aether_dsc-0.2.7}/aether_dsc/_payload/ee303363b05593f7617a9bbd46fd051fdba1ab5ed91498b223c1a2cd9c369790.bin +0 -0
- {aether_dsc-0.2.6 → aether_dsc-0.2.7}/aether_dsc/_rtc_check.py +0 -0
- {aether_dsc-0.2.6 → aether_dsc-0.2.7}/aether_dsc/_seal_impl.py +0 -0
- {aether_dsc-0.2.6 → aether_dsc-0.2.7}/aether_dsc/payload_headers.txt +0 -0
- {aether_dsc-0.2.6 → aether_dsc-0.2.7}/aether_dsc/payload_headers_host.txt +0 -0
- {aether_dsc-0.2.6 → aether_dsc-0.2.7}/aether_dsc/seal.py +0 -0
- {aether_dsc-0.2.6 → aether_dsc-0.2.7}/aether_dsc.egg-info/dependency_links.txt +0 -0
- {aether_dsc-0.2.6 → aether_dsc-0.2.7}/aether_dsc.egg-info/requires.txt +0 -0
- {aether_dsc-0.2.6 → aether_dsc-0.2.7}/aether_dsc.egg-info/top_level.txt +0 -0
- {aether_dsc-0.2.6 → aether_dsc-0.2.7}/setup.cfg +0 -0
- {aether_dsc-0.2.6 → aether_dsc-0.2.7}/tests/test_aether_dsc.py +0 -0
- {aether_dsc-0.2.6 → aether_dsc-0.2.7}/tests/test_ft_payload.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aether-dsc
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.7
|
|
4
4
|
Summary: aether-dsc — the sealed AETHER header payload: opaque, digest-named, no readable source in the wheel
|
|
5
5
|
Author: Alessandro Masat
|
|
6
6
|
License: Apache-2.0
|
|
@@ -59,6 +59,17 @@ The blob is a zlib-compressed tar — not obfuscation against a determined
|
|
|
59
59
|
reader, just enough that `strings`/`cat` on the wheel does not hand back
|
|
60
60
|
readable C++ source.
|
|
61
61
|
|
|
62
|
+
## The C reference core
|
|
63
|
+
|
|
64
|
+
`aether_dsc.c_core` is a pure-Python (standard library only) generator of
|
|
65
|
+
plain C99 functions that reproduce, bit for bit, what aether's expression
|
|
66
|
+
templates evaluate on the host: vector, matrix, quaternion and scalar
|
|
67
|
+
kinds at `f64` and `f32`. `c_core.function(kind, shape=..., width=...)`
|
|
68
|
+
returns one function (its name, text, `deps` and `libm`), and
|
|
69
|
+
`c_core.emit(names)` writes the typedefs and functions for those names and
|
|
70
|
+
everything they depend on as one block of C text. Compile it with
|
|
71
|
+
`-ffp-contract=off`.
|
|
72
|
+
|
|
62
73
|
## Threading
|
|
63
74
|
|
|
64
75
|
Free-threaded CPython (3.13t/3.14t) is supported: the compiled modules declare GIL-free operation and the GIL stays disabled after import. Any number of threads may call module-level functions, build, compile, plan and cache concurrently. Distinct objects may be used from distinct threads without synchronisation. One stateful object (a stream, capture, launcher, graph, composer, plan, pipeline, active set, host kernel or arg block) shared by several threads is memory-safe — its calls serialise and a consumed object raises — but the ORDER of those calls is the caller's responsibility, exactly as for a NumPy array or a CuPy stream. CUDA adds two rules of its own: a stream capture is begun, filled and ended by one thread, and while any capture is open no thread may synchronise the whole device (stream-level synchronisation is fine). GPU routes are supported on free-threaded 3.14; on 3.13t the CPU route runs.
|
|
@@ -40,6 +40,17 @@ The blob is a zlib-compressed tar — not obfuscation against a determined
|
|
|
40
40
|
reader, just enough that `strings`/`cat` on the wheel does not hand back
|
|
41
41
|
readable C++ source.
|
|
42
42
|
|
|
43
|
+
## The C reference core
|
|
44
|
+
|
|
45
|
+
`aether_dsc.c_core` is a pure-Python (standard library only) generator of
|
|
46
|
+
plain C99 functions that reproduce, bit for bit, what aether's expression
|
|
47
|
+
templates evaluate on the host: vector, matrix, quaternion and scalar
|
|
48
|
+
kinds at `f64` and `f32`. `c_core.function(kind, shape=..., width=...)`
|
|
49
|
+
returns one function (its name, text, `deps` and `libm`), and
|
|
50
|
+
`c_core.emit(names)` writes the typedefs and functions for those names and
|
|
51
|
+
everything they depend on as one block of C text. Compile it with
|
|
52
|
+
`-ffp-contract=off`.
|
|
53
|
+
|
|
43
54
|
## Threading
|
|
44
55
|
|
|
45
56
|
Free-threaded CPython (3.13t/3.14t) is supported: the compiled modules declare GIL-free operation and the GIL stays disabled after import. Any number of threads may call module-level functions, build, compile, plan and cache concurrently. Distinct objects may be used from distinct threads without synchronisation. One stateful object (a stream, capture, launcher, graph, composer, plan, pipeline, active set, host kernel or arg block) shared by several threads is memory-safe — its calls serialise and a consumed object raises — but the ORDER of those calls is the caller's responsibility, exactly as for a NumPy array or a CuPy stream. CUDA adds two rules of its own: a stream capture is begun, filled and ended by one thread, and while any capture is open no thread may synchronise the whole device (stream-level synchronisation is fine). GPU routes are supported on free-threaded 3.14; on 3.13t the CPU route runs.
|
|
@@ -50,7 +50,7 @@ __all__ = ["Payload", "seal", "payload", "version", "PAYLOAD_DIR"]
|
|
|
50
50
|
#: `constexpr version()`), because a sealed payload speaks for exactly one
|
|
51
51
|
#: aether checkout and a version skew between the two would silently claim
|
|
52
52
|
#: otherwise.
|
|
53
|
-
version = "0.2.
|
|
53
|
+
version = "0.2.7"
|
|
54
54
|
__version__ = version # the conventional spelling
|
|
55
55
|
|
|
56
56
|
_PAYLOAD_CACHE: Payload | None = None
|
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
# Copyright 2026 Alessandro Masat
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
"""aether's C reference core: a generator of plain C99 functions.
|
|
5
|
+
|
|
6
|
+
Each function reproduces, bit for bit, what aether's expression templates
|
|
7
|
+
evaluate on the host (see "Bit-identity formula table" in
|
|
8
|
+
docs/superpowers/specs/2026-10-16-embedded-c-target-design.md). The rules
|
|
9
|
+
that make that true:
|
|
10
|
+
|
|
11
|
+
* folds (dot, norm, sum) are right-nested, highest index outermost, with no
|
|
12
|
+
zero seed: ``e[N-1] + (e[N-2] + (... + e[0]))``;
|
|
13
|
+
* ``x / s`` is ``r = 1/s`` once, then ``r * x_k``;
|
|
14
|
+
* ``neg`` is ``x * -1``; ``norm`` is ``sqrt(ssq)`` (no hypot);
|
|
15
|
+
* nothing promotes: a ``f32`` text uses the ``f``-suffixed libm and only
|
|
16
|
+
``aether_c_f32_t`` values.
|
|
17
|
+
|
|
18
|
+
Consumers: aether's gtest suite pins the corpus header minted from these
|
|
19
|
+
texts against the C++ templates; hawk imports this module, requests
|
|
20
|
+
functions by ``(kind, shape, width)``, resolves ``deps`` transitively and
|
|
21
|
+
inlines the texts verbatim into standalone C files. The module only uses the
|
|
22
|
+
standard library. Every ``function``/``struct``/``real_typedef`` result is
|
|
23
|
+
memoised by name in one registry, which ``emit`` reads.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from dataclasses import dataclass
|
|
27
|
+
from typing import Callable, Iterable
|
|
28
|
+
|
|
29
|
+
CORE_VERSION = "aether-c/1"
|
|
30
|
+
WIDTHS = ("f64", "f32")
|
|
31
|
+
|
|
32
|
+
# width -> (C type, libm suffix)
|
|
33
|
+
_WIDTH = {"f64": ("double", ""), "f32": ("float", "f")}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True)
|
|
37
|
+
class CFunction:
|
|
38
|
+
name: str
|
|
39
|
+
text: str
|
|
40
|
+
deps: tuple[str, ...]
|
|
41
|
+
libm: tuple[str, ...]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
_REGISTRY: dict[str, CFunction] = {}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _check_width(width: str) -> None:
|
|
48
|
+
if width not in _WIDTH:
|
|
49
|
+
raise ValueError(f"unknown width {width!r}; expected one of {WIDTHS}")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
_RANK: dict[str, int] = {} # 0 width typedef, 1 struct typedef, 2 function
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _memo(item: CFunction, rank: int) -> CFunction:
|
|
56
|
+
_RANK.setdefault(item.name, rank)
|
|
57
|
+
return _REGISTRY.setdefault(item.name, item)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def real_type(width: str) -> str:
|
|
61
|
+
_check_width(width)
|
|
62
|
+
return _WIDTH[width][0]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _t(width: str) -> str:
|
|
66
|
+
return f"aether_c_{width}_t"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def real_typedef(width: str) -> CFunction:
|
|
70
|
+
_check_width(width)
|
|
71
|
+
name = _t(width)
|
|
72
|
+
guard = name.upper()
|
|
73
|
+
text = f"#ifndef {guard}\n#define {guard}\ntypedef {_WIDTH[width][0]} {name};\n#endif\n"
|
|
74
|
+
return _memo(CFunction(name, text, (), ()), 0)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _check_shape(shape) -> None:
|
|
78
|
+
"""Every extent an ``int`` (not ``bool``) >= 1; raised before anything is memoised."""
|
|
79
|
+
if not all(type(n) is int and n >= 1 for n in shape):
|
|
80
|
+
raise ValueError(f"invalid shape {shape!r}: every extent must be an int >= 1")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _struct_name(shape: tuple[int, ...], width: str) -> str:
|
|
84
|
+
_check_shape(shape)
|
|
85
|
+
if len(shape) == 1:
|
|
86
|
+
return f"aether_c_{width}_vec{shape[0]}_t"
|
|
87
|
+
if len(shape) == 2:
|
|
88
|
+
return f"aether_c_{width}_mat{shape[0]}x{shape[1]}_t"
|
|
89
|
+
raise ValueError(f"no struct for shape {shape!r}")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def struct(shape: tuple[int, ...], width: str) -> CFunction:
|
|
93
|
+
shape = tuple(shape)
|
|
94
|
+
name = _struct_name(shape, width) # validates the shape before anything is memoised
|
|
95
|
+
real = real_typedef(width)
|
|
96
|
+
if len(shape) == 1:
|
|
97
|
+
body = f"{real.name} v[{shape[0]}];"
|
|
98
|
+
else:
|
|
99
|
+
body = f"{real.name} m[{shape[0]}][{shape[1]}];"
|
|
100
|
+
guard = name.upper()
|
|
101
|
+
text = f"#ifndef {guard}\n#define {guard}\ntypedef struct {{ {body} }} {name};\n#endif\n"
|
|
102
|
+
return _memo(CFunction(name, text, (real.name,), ()), 1)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
# ---- per-family formatters; each returns (body lines, libm) ----
|
|
106
|
+
# ``cells`` are the component accessors in row-major order ("v[k]" or "m[r][c]"),
|
|
107
|
+
# so one formatter serves a vector kind and its matrix mirror; ``x`` names the
|
|
108
|
+
# operand of a unary kind.
|
|
109
|
+
|
|
110
|
+
def _fold(terms: list[str]) -> str:
|
|
111
|
+
"""Right-nested, highest index outermost, no seed."""
|
|
112
|
+
if len(terms) == 1:
|
|
113
|
+
return terms[0]
|
|
114
|
+
return f"({terms[-1]} + {_fold(terms[:-1])})"
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _cells(shape: tuple[int, ...]) -> list[str]:
|
|
118
|
+
if len(shape) == 1:
|
|
119
|
+
return [f"v[{k}]" for k in range(shape[0])]
|
|
120
|
+
return [f"m[{r}][{c}]" for r in range(shape[0]) for c in range(shape[1])]
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _fold_kind(op: str, cells: list[str], w: str, x: str) -> tuple[list[str], tuple[str, ...]]:
|
|
124
|
+
if op == "vec_dot":
|
|
125
|
+
return [f"return {_fold([f'(a.{c} * b.{c})' for c in cells])};"], ()
|
|
126
|
+
if op == "vec_norm":
|
|
127
|
+
sq = _fold([f"({x}.{c} * {x}.{c})" for c in cells])
|
|
128
|
+
return [f"return sqrt{_WIDTH[w][1]}({sq});"], (f"sqrt{_WIDTH[w][1]}",)
|
|
129
|
+
return [f"return {_fold([f'{x}.{c}' for c in cells])};"], ()
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _out_body(cells: list[str], exprs: list[str]) -> list[str]:
|
|
133
|
+
return [f"out.{c} = {e};" for c, e in zip(cells, exprs)]
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _cwise_binary(kind: str, cells: list[str], w: str, x: str) -> tuple[list[str], tuple[str, ...]]:
|
|
137
|
+
s = _WIDTH[w][1]
|
|
138
|
+
if kind in _ARITH:
|
|
139
|
+
op = _ARITH[kind]
|
|
140
|
+
return _out_body(cells, [f"(a.{c} {op} b.{c})" for c in cells]), ()
|
|
141
|
+
fn = f"{_LIBM_BINARY[kind]}{s}"
|
|
142
|
+
return _out_body(cells, [f"{fn}(a.{c}, b.{c})" for c in cells]), (fn,)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _cwise_unary(kind: str, cells: list[str], w: str, x: str) -> tuple[list[str], tuple[str, ...]]:
|
|
146
|
+
t = _t(w)
|
|
147
|
+
if kind == "vec_neg":
|
|
148
|
+
return _out_body(cells, [f"({x}.{c} * ({t})-1.0)" for c in cells]), ()
|
|
149
|
+
if kind == "vec_rsqrt":
|
|
150
|
+
fn = f"sqrt{_WIDTH[w][1]}"
|
|
151
|
+
return _out_body(cells, [f"(({t})1.0 / {fn}({x}.{c}))" for c in cells]), (fn,)
|
|
152
|
+
fn = f"{_LIBM_UNARY[kind]}{_WIDTH[w][1]}"
|
|
153
|
+
return _out_body(cells, [f"{fn}({x}.{c})" for c in cells]), (fn,)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _scale(kind: str, cells: list[str], w: str, x: str) -> tuple[list[str], tuple[str, ...]]:
|
|
157
|
+
t = _t(w)
|
|
158
|
+
if kind == "vec_scale":
|
|
159
|
+
return _out_body(cells, [f"(s * a.{c})" for c in cells]), ()
|
|
160
|
+
pre = [f"const {t} r = (({t})1.0 / s);"]
|
|
161
|
+
return pre + _out_body(cells, [f"(r * a.{c})" for c in cells]), ()
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _cross(kind: str, cells: list[str], w: str, x: str) -> tuple[list[str], tuple[str, ...]]:
|
|
165
|
+
return _out_body(cells, [
|
|
166
|
+
"((a.v[1] * b.v[2]) - (a.v[2] * b.v[1]))",
|
|
167
|
+
"((a.v[2] * b.v[0]) - (a.v[0] * b.v[2]))",
|
|
168
|
+
"((a.v[0] * b.v[1]) - (a.v[1] * b.v[0]))",
|
|
169
|
+
]), ()
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
_ARITH = {"vec_add": "+", "vec_sub": "-", "vec_mul": "*", "vec_cwise_div": "/"}
|
|
173
|
+
_LIBM_BINARY = {"vec_min": "fmin", "vec_max": "fmax", "vec_pow": "pow"}
|
|
174
|
+
_LIBM_UNARY = {"vec_abs": "fabs", "vec_sqrt": "sqrt", "vec_exp": "exp", "vec_log": "log",
|
|
175
|
+
"vec_sin": "sin", "vec_cos": "cos", "vec_tan": "tan", "vec_tanh": "tanh",
|
|
176
|
+
"vec_asin": "asin", "vec_acos": "acos", "vec_atan": "atan"}
|
|
177
|
+
|
|
178
|
+
# kind -> (arg layout, result, body formatter); layout "fold2"/"bin"=(a,b), "un"=(a) or (m),
|
|
179
|
+
# "scalar"=(a, s); result "vec" or "real". Insertion order here IS the order kinds() reports.
|
|
180
|
+
_VEC_KINDS: dict[str, tuple[str, str, Callable]] = {}
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _register(names: Iterable[str], layout: str, result: str, fmt: Callable) -> None:
|
|
184
|
+
for n in names:
|
|
185
|
+
_VEC_KINDS[n] = (layout, result, fmt)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
_register(("vec_dot",), "fold2", "real", _fold_kind)
|
|
189
|
+
_register(("vec_norm", "vec_sum"), "un", "real", _fold_kind)
|
|
190
|
+
_register(("vec_cross",), "fold2", "vec", _cross)
|
|
191
|
+
_register(tuple(_ARITH), "fold2", "vec", _cwise_binary)
|
|
192
|
+
_register(("vec_scale", "vec_div_s"), "scalar", "vec", _scale)
|
|
193
|
+
_register(("vec_min", "vec_max", "vec_pow"), "fold2", "vec", _cwise_binary)
|
|
194
|
+
_register(("vec_neg", "vec_abs", "vec_sqrt", "vec_rsqrt", "vec_exp", "vec_log", "vec_sin",
|
|
195
|
+
"vec_cos", "vec_tan", "vec_tanh", "vec_asin", "vec_acos", "vec_atan"),
|
|
196
|
+
"un", "vec", _cwise_unary)
|
|
197
|
+
assert set(_LIBM_BINARY) | set(_ARITH) | set(_LIBM_UNARY) <= set(_VEC_KINDS)
|
|
198
|
+
|
|
199
|
+
_VEC_ORDER = tuple(_VEC_KINDS)
|
|
200
|
+
# matrix mirrors: every elementwise vector kind (not the folds or cross) over m[r][c]
|
|
201
|
+
_MAT_CWISE = tuple("mat_" + k[4:] for k, v in _VEC_KINDS.items()
|
|
202
|
+
if v[1] == "vec" and k != "vec_cross")
|
|
203
|
+
_MAT_OTHER = ("mat_sum", "mat_mv", "mat_mm", "mat_transpose", "vec_outer")
|
|
204
|
+
_QUAT = ("quat_mul", "quat_conj", "quat_recip", "quat_rotate", "quat_as_pure", "quat_as_vec3")
|
|
205
|
+
_SCALARS = ("rsqrt", "sign", "clip", "remainder")
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def kinds() -> tuple[str, ...]:
|
|
209
|
+
return _VEC_ORDER + _MAT_CWISE + _MAT_OTHER + _QUAT + _SCALARS
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _render(kind: str, shape: tuple[int, ...], width: str, ret: str, name: str,
|
|
213
|
+
params: str, body: list[str], deps: tuple[str, ...], libm: tuple[str, ...]) -> CFunction:
|
|
214
|
+
head = f"/* aether-c: {kind} {shape!r} {width} {CORE_VERSION} */\n"
|
|
215
|
+
lines = "".join(f" {ln}\n" for ln in body)
|
|
216
|
+
text = f"{head}static inline {ret} {name}({params})\n{{\n{lines}}}\n"
|
|
217
|
+
return _memo(CFunction(name, text, deps, tuple(sorted(set(libm)))), 2)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _deps(width: str, *shapes: tuple[int, ...]) -> tuple[str, ...]:
|
|
221
|
+
"""Width typedef first, then each struct type in order of first use."""
|
|
222
|
+
out = [real_typedef(width).name]
|
|
223
|
+
for sh in shapes:
|
|
224
|
+
n = struct(sh, width).name
|
|
225
|
+
if n not in out:
|
|
226
|
+
out.append(n)
|
|
227
|
+
return tuple(out)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _require_rank(kind: str, shape: tuple[int, ...], rank: int) -> None:
|
|
231
|
+
if len(shape) != rank:
|
|
232
|
+
raise ValueError(f"{kind} needs a rank-{rank} shape, got {shape!r}")
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _vector_function(kind: str, shape: tuple[int, ...], width: str) -> CFunction:
|
|
236
|
+
"""A vector kind, or (kind ``mat_*``) its elementwise matrix mirror."""
|
|
237
|
+
is_mat = kind.startswith("mat_")
|
|
238
|
+
base = "vec_" + kind[4:] if is_mat else kind
|
|
239
|
+
_require_rank(kind, shape, 2 if is_mat else 1)
|
|
240
|
+
if kind == "vec_cross" and shape != (3,):
|
|
241
|
+
raise ValueError(f"vec_cross is defined only for shape (3,), got {shape!r}")
|
|
242
|
+
layout, result, fmt = _VEC_KINDS[base]
|
|
243
|
+
t, st = _t(width), _struct_name(shape, width)
|
|
244
|
+
x = "m" if is_mat else "a"
|
|
245
|
+
body, libm = fmt(base, _cells(shape), width, x)
|
|
246
|
+
params = {"fold2": f"{st} a, {st} b", "un": f"{st} {x}", "scalar": f"{st} a, {t} s"}[layout]
|
|
247
|
+
if result == "vec":
|
|
248
|
+
body = [f"{st} out;"] + body + ["return out;"]
|
|
249
|
+
prefix = st[len(f"aether_c_{width}_"):-len("_t")]
|
|
250
|
+
name = f"aether_c_{width}_{prefix}_{kind[4:]}"
|
|
251
|
+
return _render(kind, shape, width, st if result == "vec" else t, name, params, body,
|
|
252
|
+
_deps(width, shape), libm)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _matrix_function(kind: str, shape: tuple[int, ...], shape2, width: str) -> CFunction:
|
|
256
|
+
t, w = _t(width), width
|
|
257
|
+
if kind == "vec_outer":
|
|
258
|
+
_require_rank(kind, shape, 1)
|
|
259
|
+
if shape2 is None or len(shape2) != 1:
|
|
260
|
+
raise ValueError(f"vec_outer needs shape2 of rank 1, got shape={shape!r} shape2={shape2!r}")
|
|
261
|
+
out_shape = (shape[0], shape2[0])
|
|
262
|
+
ut, vt, ot = _struct_name(shape, w), _struct_name(shape2, w), _struct_name(out_shape, w)
|
|
263
|
+
body = [f"{ot} out;"] + _out_body(_cells(out_shape), [
|
|
264
|
+
f"(u.v[{r}] * v.v[{c}])" for r in range(shape[0]) for c in range(shape2[0])]) + ["return out;"]
|
|
265
|
+
name = f"aether_c_{w}_vec{shape[0]}_outer_vec{shape2[0]}"
|
|
266
|
+
return _render(kind, shape, w, ot, name, f"{ut} u, {vt} v", body,
|
|
267
|
+
_deps(w, shape, shape2, out_shape), ())
|
|
268
|
+
_require_rank(kind, shape, 2)
|
|
269
|
+
rows, cols = shape
|
|
270
|
+
mt = _struct_name(shape, w)
|
|
271
|
+
if kind == "mat_sum":
|
|
272
|
+
terms = [f"(m.{c} * ({t})1.0)" for c in _cells(shape)]
|
|
273
|
+
return _render(kind, shape, w, t, f"aether_c_{w}_mat{rows}x{cols}_sum", f"{mt} m",
|
|
274
|
+
[f"return {_fold(terms)};"], _deps(w, shape), ())
|
|
275
|
+
if kind == "mat_transpose":
|
|
276
|
+
out_shape = (cols, rows)
|
|
277
|
+
ot = _struct_name(out_shape, w)
|
|
278
|
+
body = [f"{ot} out;"] + _out_body(_cells(out_shape), [
|
|
279
|
+
f"m.m[{r}][{c}]" for c in range(cols) for r in range(rows)]) + ["return out;"]
|
|
280
|
+
return _render(kind, shape, w, ot, f"aether_c_{w}_mat{rows}x{cols}_transpose", f"{mt} m",
|
|
281
|
+
body, _deps(w, shape, out_shape), ())
|
|
282
|
+
if kind == "mat_mv":
|
|
283
|
+
vt, ot = _struct_name((cols,), w), _struct_name((rows,), w)
|
|
284
|
+
body = [f"{ot} out;"] + _out_body(_cells((rows,)), [
|
|
285
|
+
_fold([f"(m.m[{r}][{c}] * v.v[{c}])" for c in range(cols)]) for r in range(rows)]) + ["return out;"]
|
|
286
|
+
return _render(kind, shape, w, ot, f"aether_c_{w}_mat{rows}x{cols}_mv", f"{mt} m, {vt} v",
|
|
287
|
+
body, _deps(w, shape, (cols,), (rows,)), ())
|
|
288
|
+
# mat_mm
|
|
289
|
+
if shape2 is None or len(shape2) != 2 or shape2[0] != cols:
|
|
290
|
+
raise ValueError(f"mat_mm needs shape2 (K, C) with K == {cols}, got shape={shape!r} shape2={shape2!r}")
|
|
291
|
+
out_shape = (rows, shape2[1])
|
|
292
|
+
rt, ot = _struct_name(shape2, w), _struct_name(out_shape, w)
|
|
293
|
+
body = [f"{ot} out;"] + _out_body(_cells(out_shape), [
|
|
294
|
+
_fold([f"(l.m[{i}][{k}] * r.m[{k}][{j}])" for k in range(cols)])
|
|
295
|
+
for i in range(rows) for j in range(shape2[1])]) + ["return out;"]
|
|
296
|
+
name = f"aether_c_{w}_mat{rows}x{cols}_mm_mat{shape2[0]}x{shape2[1]}"
|
|
297
|
+
return _render(kind, shape, w, ot, name, f"{mt} l, {rt} r", body,
|
|
298
|
+
_deps(w, shape, shape2, out_shape), ())
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _quat_function(kind: str, shape, shape2, width: str) -> CFunction:
|
|
302
|
+
"""Quaternions are vec4 values laid out (w, x, y, z); a 3-vector is a vec3."""
|
|
303
|
+
if shape is not None or shape2 is not None:
|
|
304
|
+
raise ValueError(f"{kind} takes no shape, got shape={shape!r} shape2={shape2!r}")
|
|
305
|
+
t, w = _t(width), width
|
|
306
|
+
q4, q3 = (4,), (3,)
|
|
307
|
+
st4, st3 = _struct_name(q4, w), _struct_name(q3, w)
|
|
308
|
+
conj = lambda k: f"(q.v[{k}] * ({t})-1.0)"
|
|
309
|
+
pre: list[str] = []
|
|
310
|
+
if kind == "quat_mul":
|
|
311
|
+
# (l_i * r_j) term indices per output component, in aether's left-to-right order
|
|
312
|
+
pairs = [[(0, 0), (1, 1), (2, 2), (3, 3)], [(0, 1), (1, 0), (2, 3), (3, 2)],
|
|
313
|
+
[(0, 2), (1, 3), (2, 0), (3, 1)], [(0, 3), (1, 2), (2, 1), (3, 0)]]
|
|
314
|
+
signs = [("-", "-", "-"), ("+", "+", "-"), ("-", "+", "+"), ("+", "-", "+")]
|
|
315
|
+
exprs = []
|
|
316
|
+
for ps, sg in zip(pairs, signs):
|
|
317
|
+
e = f"(l.v[{ps[0][0]}] * r.v[{ps[0][1]}])"
|
|
318
|
+
for (i, j), op in zip(ps[1:], sg):
|
|
319
|
+
e = f"({e} {op} (l.v[{i}] * r.v[{j}]))"
|
|
320
|
+
exprs.append(e)
|
|
321
|
+
params, ret, shapes, cells = f"{st4} l, {st4} r", st4, (q4,), _cells(q4)
|
|
322
|
+
elif kind == "quat_conj":
|
|
323
|
+
exprs = ["q.v[0]"] + [conj(k) for k in (1, 2, 3)]
|
|
324
|
+
params, ret, shapes, cells = f"{st4} q", st4, (q4,), _cells(q4)
|
|
325
|
+
elif kind == "quat_recip":
|
|
326
|
+
ssq = _fold([f"(q.v[{k}] * q.v[{k}])" for k in range(4)])
|
|
327
|
+
pre = [f"const {t} r = (({t})1.0 / {ssq});"]
|
|
328
|
+
exprs = ["(r * q.v[0])"] + [f"(r * {conj(k)})" for k in (1, 2, 3)]
|
|
329
|
+
params, ret, shapes, cells = f"{st4} q", st4, (q4,), _cells(q4)
|
|
330
|
+
elif kind == "quat_rotate":
|
|
331
|
+
# component i uses j = i+1, k = i+2 (mod 3); u_j is q.v[j+1]
|
|
332
|
+
for i, n in enumerate("xyz"):
|
|
333
|
+
j, k = (i + 1) % 3, (i + 2) % 3
|
|
334
|
+
pre.append(f"const {t} t{n} = (({t})2.0 * ((q.v[{j + 1}] * v.v[{k}]) - (q.v[{k + 1}] * v.v[{j}])));")
|
|
335
|
+
exprs = []
|
|
336
|
+
for i in range(3):
|
|
337
|
+
j, k = (i + 1) % 3, (i + 2) % 3
|
|
338
|
+
exprs.append(f"((v.v[{i}] + (q.v[0] * t{'xyz'[i]})) + "
|
|
339
|
+
f"((q.v[{j + 1}] * t{'xyz'[k]}) - (q.v[{k + 1}] * t{'xyz'[j]})))")
|
|
340
|
+
params, ret, shapes, cells = f"{st4} q, {st3} v", st3, (q4, q3), _cells(q3)
|
|
341
|
+
elif kind == "quat_as_pure":
|
|
342
|
+
exprs = [f"({t})0.0"] + [f"v.v[{k}]" for k in range(3)]
|
|
343
|
+
params, ret, shapes, cells = f"{st3} v", st4, (q3, q4), _cells(q4)
|
|
344
|
+
else: # quat_as_vec3
|
|
345
|
+
exprs = [f"q.v[{k}]" for k in (1, 2, 3)]
|
|
346
|
+
params, ret, shapes, cells = f"{st4} q", st3, (q4, q3), _cells(q3)
|
|
347
|
+
body = pre + [f"{ret} out;"] + _out_body(cells, exprs) + ["return out;"]
|
|
348
|
+
return _render(kind, q4, w, ret, f"aether_c_{w}_quat_{kind[5:]}", params, body,
|
|
349
|
+
_deps(w, *shapes), ())
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def _scalar_function(kind: str, width: str) -> CFunction:
|
|
353
|
+
t, s = _t(width), _WIDTH[width][1]
|
|
354
|
+
real = real_typedef(width)
|
|
355
|
+
sq, fm, cs = f"sqrt{s}", f"fmod{s}", f"copysign{s}"
|
|
356
|
+
if kind == "rsqrt":
|
|
357
|
+
params, body, libm = f"{t} x", [f"return (({t})1.0 / {sq}(x));"], (sq,)
|
|
358
|
+
elif kind == "sign":
|
|
359
|
+
params, libm = f"{t} x", ()
|
|
360
|
+
body = [f"return (x > ({t})0.0) ? ({t})1.0 : ((x < ({t})0.0) ? ({t})-1.0 : ({t})0.0);"]
|
|
361
|
+
elif kind == "clip":
|
|
362
|
+
params, libm = f"{t} x, {t} lo, {t} hi", ()
|
|
363
|
+
body = [f"const {t} t = ((x >= lo) || (x != x)) ? x : lo;",
|
|
364
|
+
"return ((t <= hi) || (t != t)) ? t : hi;"]
|
|
365
|
+
else:
|
|
366
|
+
params, libm = f"{t} a, {t} b", (fm, cs)
|
|
367
|
+
body = [f"{t} m = {fm}(a, b);",
|
|
368
|
+
f"if (m != ({t})0.0) {{",
|
|
369
|
+
f" if ((b < ({t})0.0) != (m < ({t})0.0)) {{",
|
|
370
|
+
" m += b;",
|
|
371
|
+
" }",
|
|
372
|
+
"} else {",
|
|
373
|
+
f" m = {cs}(({t})0.0, b);",
|
|
374
|
+
"}",
|
|
375
|
+
"return m;"]
|
|
376
|
+
return _render(kind, (), width, t, f"aether_c_{width}_{kind}", params, body, (real.name,), libm)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def function(kind: str, *, shape=None, shape2=None, width: str = "f64") -> CFunction:
|
|
380
|
+
_check_width(width)
|
|
381
|
+
if kind in _SCALARS:
|
|
382
|
+
if shape is not None or shape2 is not None:
|
|
383
|
+
raise ValueError(f"{kind} takes no shape, got shape={shape!r} shape2={shape2!r}")
|
|
384
|
+
return _scalar_function(kind, width)
|
|
385
|
+
if kind not in kinds():
|
|
386
|
+
raise ValueError(f"unknown kind {kind!r}; known kinds: {', '.join(kinds())}")
|
|
387
|
+
if kind in _QUAT:
|
|
388
|
+
return _quat_function(kind, shape, shape2, width)
|
|
389
|
+
if shape is None:
|
|
390
|
+
raise ValueError(f"{kind} needs a shape")
|
|
391
|
+
shape = tuple(shape)
|
|
392
|
+
shape2 = None if shape2 is None else tuple(shape2)
|
|
393
|
+
_check_shape(shape)
|
|
394
|
+
if shape2 is not None:
|
|
395
|
+
_check_shape(shape2)
|
|
396
|
+
if kind not in ("mat_mm", "vec_outer"):
|
|
397
|
+
raise ValueError(f"{kind} takes no shape2, got shape2={shape2!r}")
|
|
398
|
+
if kind in _VEC_KINDS or kind in _MAT_CWISE:
|
|
399
|
+
return _vector_function(kind, shape, width)
|
|
400
|
+
return _matrix_function(kind, shape, shape2, width)
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def emit(names: Iterable[str]) -> str:
|
|
404
|
+
"""The closure of ``names``: typedefs, then structs, then functions, each once."""
|
|
405
|
+
order: list[str] = []
|
|
406
|
+
seen: set[str] = set()
|
|
407
|
+
|
|
408
|
+
def visit(name: str) -> None:
|
|
409
|
+
if name in seen:
|
|
410
|
+
return
|
|
411
|
+
if name not in _REGISTRY:
|
|
412
|
+
raise KeyError(name)
|
|
413
|
+
seen.add(name)
|
|
414
|
+
for dep in _REGISTRY[name].deps:
|
|
415
|
+
visit(dep)
|
|
416
|
+
order.append(name)
|
|
417
|
+
|
|
418
|
+
for name in names:
|
|
419
|
+
visit(name)
|
|
420
|
+
order.sort(key=_RANK.__getitem__) # stable: dependency order kept within a class
|
|
421
|
+
return "\n".join(_REGISTRY[n].text for n in order)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aether-dsc
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.7
|
|
4
4
|
Summary: aether-dsc — the sealed AETHER header payload: opaque, digest-named, no readable source in the wheel
|
|
5
5
|
Author: Alessandro Masat
|
|
6
6
|
License: Apache-2.0
|
|
@@ -59,6 +59,17 @@ The blob is a zlib-compressed tar — not obfuscation against a determined
|
|
|
59
59
|
reader, just enough that `strings`/`cat` on the wheel does not hand back
|
|
60
60
|
readable C++ source.
|
|
61
61
|
|
|
62
|
+
## The C reference core
|
|
63
|
+
|
|
64
|
+
`aether_dsc.c_core` is a pure-Python (standard library only) generator of
|
|
65
|
+
plain C99 functions that reproduce, bit for bit, what aether's expression
|
|
66
|
+
templates evaluate on the host: vector, matrix, quaternion and scalar
|
|
67
|
+
kinds at `f64` and `f32`. `c_core.function(kind, shape=..., width=...)`
|
|
68
|
+
returns one function (its name, text, `deps` and `libm`), and
|
|
69
|
+
`c_core.emit(names)` writes the typedefs and functions for those names and
|
|
70
|
+
everything they depend on as one block of C text. Compile it with
|
|
71
|
+
`-ffp-contract=off`.
|
|
72
|
+
|
|
62
73
|
## Threading
|
|
63
74
|
|
|
64
75
|
Free-threaded CPython (3.13t/3.14t) is supported: the compiled modules declare GIL-free operation and the GIL stays disabled after import. Any number of threads may call module-level functions, build, compile, plan and cache concurrently. Distinct objects may be used from distinct threads without synchronisation. One stateful object (a stream, capture, launcher, graph, composer, plan, pipeline, active set, host kernel or arg block) shared by several threads is memory-safe — its calls serialise and a consumed object raises — but the ORDER of those calls is the caller's responsibility, exactly as for a NumPy array or a CuPy stream. CUDA adds two rules of its own: a stream capture is begun, filled and ended by one thread, and while any capture is open no thread may synchronise the whole device (stream-level synchronisation is fine). GPU routes are supported on free-threaded 3.14; on 3.13t the CPU route runs.
|
|
@@ -6,6 +6,7 @@ aether_dsc/__init__.py
|
|
|
6
6
|
aether_dsc/_core.py
|
|
7
7
|
aether_dsc/_rtc_check.py
|
|
8
8
|
aether_dsc/_seal_impl.py
|
|
9
|
+
aether_dsc/c_core.py
|
|
9
10
|
aether_dsc/payload_headers.txt
|
|
10
11
|
aether_dsc/payload_headers_host.txt
|
|
11
12
|
aether_dsc/seal.py
|
|
@@ -16,4 +17,5 @@ aether_dsc.egg-info/requires.txt
|
|
|
16
17
|
aether_dsc.egg-info/top_level.txt
|
|
17
18
|
aether_dsc/_payload/ee303363b05593f7617a9bbd46fd051fdba1ab5ed91498b223c1a2cd9c369790.bin
|
|
18
19
|
tests/test_aether_dsc.py
|
|
20
|
+
tests/test_c_core.py
|
|
19
21
|
tests/test_ft_payload.py
|
|
@@ -14,7 +14,7 @@ name = "aether-dsc"
|
|
|
14
14
|
# aether-dsc ships a SEALED payload of that exact checkout, so a version skew
|
|
15
15
|
# between the two would silently claim a payload speaks for a different
|
|
16
16
|
# aether than it does.
|
|
17
|
-
version = "0.2.
|
|
17
|
+
version = "0.2.7"
|
|
18
18
|
description = "aether-dsc — the sealed AETHER header payload: opaque, digest-named, no readable source in the wheel"
|
|
19
19
|
readme = "README.md"
|
|
20
20
|
requires-python = ">=3.9"
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# Copyright 2026 Alessandro Masat
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
import re, subprocess, shutil, pytest
|
|
4
|
+
from aether_dsc import c_core as C
|
|
5
|
+
|
|
6
|
+
F64_DOT3 = ("static inline aether_c_f64_t aether_c_f64_vec3_dot(aether_c_f64_vec3_t a, aether_c_f64_vec3_t b)\n"
|
|
7
|
+
"{\n return ((a.v[2] * b.v[2]) + ((a.v[1] * b.v[1]) + (a.v[0] * b.v[0])));\n}\n")
|
|
8
|
+
|
|
9
|
+
def test_vec3_dot_is_right_nested_with_no_seed():
|
|
10
|
+
f = C.function("vec_dot", shape=(3,), width="f64")
|
|
11
|
+
assert f.name == "aether_c_f64_vec3_dot" and f.text.endswith(F64_DOT3)
|
|
12
|
+
assert f.text.startswith("/* aether-c: vec_dot (3,) f64 aether-c/1 */\n")
|
|
13
|
+
assert f.deps == ("aether_c_f64_t", "aether_c_f64_vec3_t") and f.libm == ()
|
|
14
|
+
|
|
15
|
+
def test_vec3_norm_is_sqrt_of_right_nested_squares():
|
|
16
|
+
assert "return sqrt(((a.v[2] * a.v[2]) + ((a.v[1] * a.v[1]) + (a.v[0] * a.v[0]))));" in C.function("vec_norm", shape=(3,)).text
|
|
17
|
+
|
|
18
|
+
def test_vec1_dot_is_one_product():
|
|
19
|
+
assert "return (a.v[0] * b.v[0]);" in C.function("vec_dot", shape=(1,)).text
|
|
20
|
+
|
|
21
|
+
def test_div_s_multiplies_by_one_reciprocal():
|
|
22
|
+
t = C.function("vec_div_s", shape=(2,)).text
|
|
23
|
+
assert "const aether_c_f64_t r = ((aether_c_f64_t)1.0 / s);" in t and "out.v[1] = (r * a.v[1]);" in t
|
|
24
|
+
|
|
25
|
+
def test_neg_multiplies_by_minus_one():
|
|
26
|
+
assert "out.v[0] = (a.v[0] * (aether_c_f64_t)-1.0);" in C.function("vec_neg", shape=(1,)).text
|
|
27
|
+
|
|
28
|
+
def test_min_keeps_operand_order():
|
|
29
|
+
assert "out.v[0] = fmin(a.v[0], b.v[0]);" in C.function("vec_min", shape=(1,)).text
|
|
30
|
+
|
|
31
|
+
def test_cross_formulas_and_shape_guard():
|
|
32
|
+
t = C.function("vec_cross", shape=(3,)).text
|
|
33
|
+
assert "out.v[0] = ((a.v[1] * b.v[2]) - (a.v[2] * b.v[1]));" in t
|
|
34
|
+
assert "out.v[1] = ((a.v[2] * b.v[0]) - (a.v[0] * b.v[2]));" in t
|
|
35
|
+
assert "out.v[2] = ((a.v[0] * b.v[1]) - (a.v[1] * b.v[0]));" in t
|
|
36
|
+
with pytest.raises(ValueError, match=r"\(4,\)"):
|
|
37
|
+
C.function("vec_cross", shape=(4,))
|
|
38
|
+
|
|
39
|
+
def test_scalar_formulas():
|
|
40
|
+
assert "return ((aether_c_f64_t)1.0 / sqrt(x));" in C.function("rsqrt").text
|
|
41
|
+
assert "const aether_c_f64_t t = ((x >= lo) || (x != x)) ? x : lo;" in C.function("clip").text
|
|
42
|
+
assert "return ((t <= hi) || (t != t)) ? t : hi;" in C.function("clip").text
|
|
43
|
+
r = C.function("remainder").text
|
|
44
|
+
assert "aether_c_f64_t m = fmod(a, b);" in r and "m = copysign((aether_c_f64_t)0.0, b);" in r
|
|
45
|
+
assert C.function("remainder").libm == ("copysign", "fmod")
|
|
46
|
+
|
|
47
|
+
def test_f32_never_promotes():
|
|
48
|
+
t = C.function("vec_norm", shape=(3,), width="f32").text
|
|
49
|
+
assert "sqrtf(" in t and "aether_c_f32_t" in t and "double" not in t
|
|
50
|
+
assert C.function("vec_norm", shape=(3,), width="f32").libm == ("sqrtf",)
|
|
51
|
+
|
|
52
|
+
def test_struct_typedefs_are_guarded():
|
|
53
|
+
s = C.struct((3,), "f64")
|
|
54
|
+
assert s.text == "#ifndef AETHER_C_F64_VEC3_T\n#define AETHER_C_F64_VEC3_T\ntypedef struct { aether_c_f64_t v[3]; } aether_c_f64_vec3_t;\n#endif\n"
|
|
55
|
+
|
|
56
|
+
MAT_SHAPES = ((2, 2), (3, 3), (4, 4), (2, 3), (3, 2), (1, 3))
|
|
57
|
+
MM_PAIRS = (((2, 3), (3, 2)), ((3, 3), (3, 3)), ((2, 2), (2, 2)))
|
|
58
|
+
OUTER_PAIRS = (((2,), (3,)), ((3,), (3,)))
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _requests(width):
|
|
62
|
+
"""Every (kind, kwargs) the broad tests exercise, derived from kinds()."""
|
|
63
|
+
for kind in C.kinds():
|
|
64
|
+
if kind == "vec_outer":
|
|
65
|
+
for s, s2 in OUTER_PAIRS:
|
|
66
|
+
yield kind, dict(shape=s, shape2=s2, width=width)
|
|
67
|
+
elif kind == "mat_mm":
|
|
68
|
+
for s, s2 in MM_PAIRS:
|
|
69
|
+
yield kind, dict(shape=s, shape2=s2, width=width)
|
|
70
|
+
elif kind.startswith("mat_"):
|
|
71
|
+
for s in MAT_SHAPES:
|
|
72
|
+
yield kind, dict(shape=s, width=width)
|
|
73
|
+
elif kind.startswith("quat_"):
|
|
74
|
+
yield kind, dict(width=width)
|
|
75
|
+
elif kind.startswith("vec_"):
|
|
76
|
+
for s in ((1,), (2,), (3,), (4,), (6,)):
|
|
77
|
+
if kind == "vec_cross" and s != (3,): continue
|
|
78
|
+
yield kind, dict(shape=s, width=width)
|
|
79
|
+
else:
|
|
80
|
+
yield kind, dict(width=width)
|
|
81
|
+
|
|
82
|
+
def test_texts_are_deterministic_c99_and_c_plus_plus_safe():
|
|
83
|
+
for width in C.WIDTHS:
|
|
84
|
+
for kind, kw in _requests(width):
|
|
85
|
+
a, b = C.function(kind, **kw), C.function(kind, **kw)
|
|
86
|
+
assert a == b and "//" not in a.text and "static inline" in a.text
|
|
87
|
+
assert a.deps[0] == f"aether_c_{width}_t"
|
|
88
|
+
|
|
89
|
+
def test_emit_orders_the_closure_typedefs_first_each_once():
|
|
90
|
+
names = [C.function("vec_norm", shape=(3,)).name, C.function("vec_dot", shape=(3,)).name]
|
|
91
|
+
text = C.emit(names)
|
|
92
|
+
assert text.index("typedef double aether_c_f64_t;") < text.index("aether_c_f64_vec3_t;") < text.index("aether_c_f64_vec3_norm(")
|
|
93
|
+
assert text.count("typedef struct { aether_c_f64_t v[3]; }") == 1
|
|
94
|
+
|
|
95
|
+
@pytest.mark.skipif(shutil.which("cc") is None, reason="no C compiler")
|
|
96
|
+
def test_emitted_closure_compiles_as_strict_c99(tmp_path):
|
|
97
|
+
names = [C.function(k, **kw).name for w in C.WIDTHS for k, kw in _requests(w)]
|
|
98
|
+
src = tmp_path / "t.c"
|
|
99
|
+
src.write_text("#include <math.h>\n" + C.emit(names))
|
|
100
|
+
subprocess.run(["cc", "-std=c99", "-pedantic", "-Wall", "-Wextra", "-Wconversion", "-Wsign-conversion",
|
|
101
|
+
"-Wdouble-promotion", "-Wno-unused-function", "-Werror", "-fsyntax-only", str(src)], check=True)
|
|
102
|
+
|
|
103
|
+
def test_mat2x2_sum_is_the_ones_matdot_fold():
|
|
104
|
+
t = C.function("mat_sum", shape=(2, 2)).text
|
|
105
|
+
assert "return ((m.m[1][1] * (aether_c_f64_t)1.0) + ((m.m[1][0] * (aether_c_f64_t)1.0) + ((m.m[0][1] * (aether_c_f64_t)1.0) + (m.m[0][0] * (aether_c_f64_t)1.0))));" in t
|
|
106
|
+
|
|
107
|
+
def test_mat2x2_mv_row_fold():
|
|
108
|
+
t = C.function("mat_mv", shape=(2, 2)).text
|
|
109
|
+
assert "out.v[0] = ((m.m[0][1] * v.v[1]) + (m.m[0][0] * v.v[0]));" in t
|
|
110
|
+
|
|
111
|
+
def test_mat1x3_mv():
|
|
112
|
+
assert "out.v[0] = ((m.m[0][2] * v.v[2]) + ((m.m[0][1] * v.v[1]) + (m.m[0][0] * v.v[0])));" in C.function("mat_mv", shape=(1, 3)).text
|
|
113
|
+
|
|
114
|
+
def test_mat_mm_names_both_shapes_and_folds_over_k():
|
|
115
|
+
f = C.function("mat_mm", shape=(2, 3), shape2=(3, 2))
|
|
116
|
+
assert f.name == "aether_c_f64_mat2x3_mm_mat3x2"
|
|
117
|
+
assert "out.m[0][0] = ((l.m[0][2] * r.m[2][0]) + ((l.m[0][1] * r.m[1][0]) + (l.m[0][0] * r.m[0][0])));" in f.text
|
|
118
|
+
assert set(f.deps) >= {"aether_c_f64_mat2x3_t", "aether_c_f64_mat3x2_t", "aether_c_f64_mat2x2_t"}
|
|
119
|
+
assert f.deps[0] == "aether_c_f64_t"
|
|
120
|
+
|
|
121
|
+
def test_outer_and_transpose():
|
|
122
|
+
assert "out.m[1][0] = (u.v[1] * v.v[0]);" in C.function("vec_outer", shape=(2,), shape2=(3,)).text
|
|
123
|
+
assert C.function("vec_outer", shape=(2,), shape2=(3,)).name == "aether_c_f64_vec2_outer_vec3"
|
|
124
|
+
assert "out.m[2][0] = m.m[0][2];" in C.function("mat_transpose", shape=(1, 3)).text
|
|
125
|
+
|
|
126
|
+
def test_mat_cwise_mirror_the_vector_templates():
|
|
127
|
+
assert "out.m[1][0] = (s * a.m[1][0]);" in C.function("mat_scale", shape=(2, 2)).text
|
|
128
|
+
t = C.function("mat_div_s", shape=(2, 2)).text
|
|
129
|
+
assert "const aether_c_f64_t r = ((aether_c_f64_t)1.0 / s);" in t and "out.m[1][1] = (r * a.m[1][1]);" in t
|
|
130
|
+
assert "out.m[0][1] = (m.m[0][1] * (aether_c_f64_t)-1.0);" in C.function("mat_neg", shape=(1, 3)).text
|
|
131
|
+
assert "out.m[0][0] = ((aether_c_f32_t)1.0 / sqrtf(m.m[0][0]));" in C.function("mat_rsqrt", shape=(1, 1), width="f32").text
|
|
132
|
+
assert "out.m[0][0] = fmin(a.m[0][0], b.m[0][0]);" in C.function("mat_min", shape=(1, 3)).text
|
|
133
|
+
assert "out.m[0][2] = fabsf(m.m[0][2]);" in C.function("mat_abs", shape=(1, 3), width="f32").text
|
|
134
|
+
assert "out.m[0][0] = (a.m[0][0] + b.m[0][0]);" in C.function("mat_add", shape=(2, 3)).text
|
|
135
|
+
|
|
136
|
+
def test_matrix_shape_errors():
|
|
137
|
+
with pytest.raises(ValueError, match=r"\(3,\)"):
|
|
138
|
+
C.function("mat_sum", shape=(3,))
|
|
139
|
+
with pytest.raises(ValueError, match=r"\(2, 3\).*\(2, 3\)"):
|
|
140
|
+
C.function("mat_mm", shape=(2, 3), shape2=(2, 3))
|
|
141
|
+
with pytest.raises(ValueError, match=r"\(2, 2\)"):
|
|
142
|
+
C.function("mat_mm", shape=(2, 2))
|
|
143
|
+
with pytest.raises(ValueError, match=r"\(2,\)"):
|
|
144
|
+
C.function("vec_outer", shape=(2,))
|
|
145
|
+
|
|
146
|
+
def test_kinds_order_matrix_between_vectors_and_scalars():
|
|
147
|
+
k = C.kinds()
|
|
148
|
+
assert k.index("vec_atan") < k.index("mat_add") < k.index("mat_atan") < k.index("mat_sum") < k.index("rsqrt")
|
|
149
|
+
assert k[-4:] == ("rsqrt", "sign", "clip", "remainder") and k[-11] == "vec_outer" and k[-10:-4] == tuple(x for x in k if x.startswith("quat_"))
|
|
150
|
+
assert len(set(k)) == len(k)
|
|
151
|
+
|
|
152
|
+
def test_unknown_kind_and_name():
|
|
153
|
+
with pytest.raises(ValueError, match="vec_dot"):
|
|
154
|
+
C.function("nope")
|
|
155
|
+
with pytest.raises(KeyError, match="no_such"):
|
|
156
|
+
C.emit(["no_such"])
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def test_quat_mul_hamilton_left_to_right():
|
|
160
|
+
t = C.function("quat_mul").text
|
|
161
|
+
assert "out.v[0] = ((((l.v[0] * r.v[0]) - (l.v[1] * r.v[1])) - (l.v[2] * r.v[2])) - (l.v[3] * r.v[3]));" in t
|
|
162
|
+
assert "out.v[1] = ((((l.v[0] * r.v[1]) + (l.v[1] * r.v[0])) + (l.v[2] * r.v[3])) - (l.v[3] * r.v[2]));" in t
|
|
163
|
+
assert "out.v[2] = ((((l.v[0] * r.v[2]) - (l.v[1] * r.v[3])) + (l.v[2] * r.v[0])) + (l.v[3] * r.v[1]));" in t
|
|
164
|
+
assert "out.v[3] = ((((l.v[0] * r.v[3]) + (l.v[1] * r.v[2])) - (l.v[2] * r.v[1])) + (l.v[3] * r.v[0]));" in t
|
|
165
|
+
|
|
166
|
+
def test_quat_rotate_expands_as_aether_does():
|
|
167
|
+
t = C.function("quat_rotate").text
|
|
168
|
+
assert "const aether_c_f64_t tx = ((aether_c_f64_t)2.0 * ((q.v[2] * v.v[2]) - (q.v[3] * v.v[1])));" in t
|
|
169
|
+
assert "out.v[0] = ((v.v[0] + (q.v[0] * tx)) + ((q.v[2] * tz) - (q.v[3] * ty)));" in t
|
|
170
|
+
|
|
171
|
+
def test_quat_recip_scales_the_conjugate_by_one_reciprocal():
|
|
172
|
+
t = C.function("quat_recip").text
|
|
173
|
+
assert "const aether_c_f64_t r = ((aether_c_f64_t)1.0 / ((q.v[3] * q.v[3]) + ((q.v[2] * q.v[2]) + ((q.v[1] * q.v[1]) + (q.v[0] * q.v[0])))));" in t
|
|
174
|
+
assert "out.v[0] = (r * q.v[0]);" in t and "out.v[1] = (r * (q.v[1] * (aether_c_f64_t)-1.0));" in t
|
|
175
|
+
|
|
176
|
+
def test_quat_as_pure_and_as_vec3():
|
|
177
|
+
assert "out.v[0] = (aether_c_f64_t)0.0;" in C.function("quat_as_pure").text
|
|
178
|
+
assert "out.v[2] = q.v[3];" in C.function("quat_as_vec3").text
|
|
179
|
+
|
|
180
|
+
def test_quat_names_types_deps_and_shape_rejection():
|
|
181
|
+
f = C.function("quat_rotate", width="f32")
|
|
182
|
+
assert f.name == "aether_c_f32_quat_rotate"
|
|
183
|
+
assert f.text.startswith("/* aether-c: quat_rotate (4,) f32 aether-c/1 */\n")
|
|
184
|
+
assert "static inline aether_c_f32_vec3_t aether_c_f32_quat_rotate(aether_c_f32_vec4_t q, aether_c_f32_vec3_t v)" in f.text
|
|
185
|
+
assert f.deps == ("aether_c_f32_t", "aether_c_f32_vec4_t", "aether_c_f32_vec3_t")
|
|
186
|
+
assert C.function("quat_as_pure").deps == ("aether_c_f64_t", "aether_c_f64_vec3_t", "aether_c_f64_vec4_t")
|
|
187
|
+
assert "const aether_c_f32_t r = ((aether_c_f32_t)1.0 /" in C.function("quat_recip", width="f32").text
|
|
188
|
+
with pytest.raises(ValueError):
|
|
189
|
+
C.function("quat_mul", shape=(4,))
|
|
190
|
+
with pytest.raises(ValueError):
|
|
191
|
+
C.function("quat_mul", shape2=(4,))
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
_LIBM_CALL = re.compile(r"\b(sqrt|fmin|fmax|pow|fabs|exp|log|sin|cos|tan|tanh|asin|acos|atan|fmod|copysign)f?\(")
|
|
195
|
+
|
|
196
|
+
def test_every_registered_name_audits_deps_libm_and_width_purity():
|
|
197
|
+
for width in C.WIDTHS:
|
|
198
|
+
for kind, kw in _requests(width):
|
|
199
|
+
f = C.function(kind, **kw)
|
|
200
|
+
for dep in f.deps:
|
|
201
|
+
C.emit([dep]) # KeyError if a dep is not registered
|
|
202
|
+
calls = set(re.findall(r"\b((?:sqrt|fmin|fmax|pow|fabs|exp|log|sin|cos|tan|tanh|asin|acos|atan|fmod|copysign)f?)\(", f.text))
|
|
203
|
+
assert set(f.libm) == calls and len(f.libm) == len(set(f.libm)), (f.name, f.libm, calls)
|
|
204
|
+
if width == "f32":
|
|
205
|
+
assert all(c.endswith("f") for c in calls), f.name
|
|
206
|
+
assert not re.search(r"\bdouble\b", f.text), f.name
|
|
207
|
+
else:
|
|
208
|
+
assert not re.search(r"\bfloat\b", f.text), f.name
|
|
209
|
+
|
|
210
|
+
def test_invalid_shapes_raise_and_register_nothing():
|
|
211
|
+
before = dict(C._REGISTRY)
|
|
212
|
+
for kind, kw in (("vec_add", dict(shape=(0,))), ("vec_add", dict(shape=(-2,))),
|
|
213
|
+
("vec_dot", dict(shape=(0,))), ("mat_mv", dict(shape=(0, 3))),
|
|
214
|
+
("vec_add", dict(shape=(True,))), ("vec_add", dict(shape=(2.0,))),
|
|
215
|
+
("rsqrt", dict(shape=(3,))), ("clip", dict(shape2=(3,))),
|
|
216
|
+
("vec_add", dict(shape=(3,), shape2=(3,)))):
|
|
217
|
+
with pytest.raises(ValueError):
|
|
218
|
+
C.function(kind, **kw)
|
|
219
|
+
with pytest.raises(ValueError):
|
|
220
|
+
C.struct((0,), "f32")
|
|
221
|
+
assert C._REGISTRY == before
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|