rns-engine 0.4.1__tar.gz → 0.5.0__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rns_engine
3
- Version: 0.4.1
3
+ Version: 0.5.0
4
4
  Summary: Exact RNS engine with session/cache API.
5
5
  Author: Evan Wesley
6
6
  License-Expression: AGPL-3.0-only
@@ -109,12 +109,86 @@ hot = s.hot_loop_affine(x, multiplier=1_000_003, addend=7, iterations=1000)
109
109
 
110
110
  ---
111
111
 
112
+ ## Centered signed arithmetic and range certificates
113
+
114
+ The native core computes exact residues modulo `M`. Version 0.5 adds a canonical signed view over:
115
+
116
+ ```text
117
+ [-M/2, M/2 - 1]
118
+ =
119
+ [-17,871,445,090,598,912, 17,871,445,090,598,911]
120
+ ```
121
+
122
+ ```python
123
+ import numpy as np
124
+ import rns_engine as rns
125
+
126
+ values = np.array([-5, 0, 7], dtype=np.int64)
127
+ rails = rns.encode_signed(values)
128
+ round_trip = rns.decode_signed(*rails)
129
+
130
+ assert np.array_equal(round_trip, values)
131
+ ```
132
+
133
+ `encode_signed(...)` rejects values outside the centered interval instead of silently wrapping them modulo `M`.
134
+
135
+ A centered decoding is a canonical interpretation of a residue. It is **not by itself proof that an unknown mathematical result did not wrap**. Unique signed reconstruction requires an independent strict magnitude bound:
136
+
137
+ ```text
138
+ |result| < M/2
139
+ ```
140
+
141
+ Use a range certificate before claiming that a residue is the unique signed integer result:
142
+
143
+ ```python
144
+ certificate = rns.certify_signed_bound(max_abs_bound=1_000_000)
145
+ certificate.require_unique()
146
+
147
+ print(certificate.headroom)
148
+ print(certificate.minimum_required_modulus)
149
+ ```
150
+
151
+ For a signed dot product or one GEMM output entry, the engine can build the worst-case receipt directly:
152
+
153
+ ```python
154
+ certificate = rns.certify_signed_dot_bound(
155
+ inner_dimension=5120,
156
+ left_abs_bound=127,
157
+ right_abs_bound=127,
158
+ )
159
+
160
+ assert certificate.max_abs_bound == 82_580_480
161
+ assert certificate.unique
162
+ certificate.require_unique()
163
+ ```
164
+
165
+ This uses the exact conservative law:
166
+
167
+ ```text
168
+ output bound
169
+ =
170
+ inner_dimension × left bound × right bound + addend bound
171
+ ```
172
+
173
+ `SignedSession` exposes the existing residue arithmetic with signed external values:
174
+
175
+ ```python
176
+ s = rns.SignedSession()
177
+ a = s.encode_signed([-5, 7])
178
+ b = s.encode_signed([3, -10])
179
+ out = s.decode_signed(s.add(a, b))
180
+ ```
181
+
182
+ ---
183
+
112
184
  ## Core API
113
185
 
114
186
  ### Encoded rail API
115
187
 
116
188
  * `rns.encode(x)` → `(r0, r1, r2, r3)`
117
189
  * `rns.decode(r0, r1, r2, r3)` → `uint64[]`
190
+ * `rns.encode_signed(x)` → strict centered signed encoding
191
+ * `rns.decode_signed(r0, r1, r2, r3)` → canonical `int64[]`
118
192
  * `rns.add(*ea, *eb)`
119
193
  * `rns.sub(*ea, *eb)`
120
194
  * `rns.mul(*ea, *eb)`
@@ -122,6 +196,15 @@ hot = s.hot_loop_affine(x, multiplier=1_000_003, addend=7, iterations=1000)
122
196
  * `rns.fma(*ea, *eb, *ec)`
123
197
  * `rns.op(*ea, *eb, code)` where `0=add 1=mul 2=sub 3=div`
124
198
 
199
+ ### Range-certificate API
200
+
201
+ * `rns.certify_signed_bound(max_abs_bound)`
202
+ * `rns.certify_signed_dot_bound(inner_dimension, left_abs_bound, right_abs_bound, addend_abs_bound=0)`
203
+ * `SignedRangeCertificate.unique`
204
+ * `SignedRangeCertificate.headroom`
205
+ * `SignedRangeCertificate.minimum_required_modulus`
206
+ * `SignedRangeCertificate.require_unique()`
207
+
125
208
  ### Scalar-broadcast encoded API
126
209
 
127
210
  These avoid materializing full constant arrays in Python:
@@ -159,6 +242,7 @@ Auto-dispatch variants:
159
242
  ### High-level API
160
243
 
161
244
  * `rns.Session`
245
+ * `rns.SignedSession`
162
246
  * `rns.SessionCache`
163
247
  * `rns.EncodedArray`
164
248
 
@@ -208,7 +292,9 @@ def make_invertible_divisor(b):
208
292
  ## Data model
209
293
 
210
294
  * input arrays to `encode(...)` are treated as `uint64`
211
- * values outside `[0, M)` are reduced mod `M` during encode
295
+ * values outside `[0, M)` are reduced mod `M` during unsigned encode
296
+ * `encode_signed(...)` accepts only integers in `[-M/2, M/2 - 1]` and refuses silent wrapping
297
+ * `decode_signed(...)` returns the canonical centered `int64` view
212
298
  * rails are returned as:
213
299
  * `r0`: `uint16`
214
300
  * `r1`: `uint16`
@@ -251,7 +337,8 @@ Workload: `affine_repeat_u64_io(x, 1_000_003, 7, iterations=1000)`
251
337
  ### Verification status
252
338
 
253
339
  * correctness sanity checks passed
254
- * full test suite passed: **50 / 50**
340
+ * the full suite is exercised on Linux, macOS, and Windows across Python 3.10–3.13
341
+ * built wheels are installed and tested before release
255
342
 
256
343
  ---
257
344
 
@@ -310,6 +397,9 @@ import rns_engine as rns
310
397
  rns.info()
311
398
 
312
399
  rns.M
400
+ rns.HALF_M
401
+ rns.SIGNED_MIN
402
+ rns.SIGNED_MAX
313
403
  rns.M0
314
404
  rns.M1
315
405
  rns.M2
@@ -321,9 +411,13 @@ rns.HAS_AVX2
321
411
 
322
412
  ## Current release
323
413
 
324
- ### v0.4.1
414
+ ### v0.5.0
325
415
 
326
- * Maintenance release: repaired packaging metadata, cross-platform wheel builds, and Python 3.10–3.13 CI; arithmetic core unchanged
416
+ * canonical centered signed encoding and decoding
417
+ * strict signed input validation with no silent modular wrapping
418
+ * signed uniqueness certificates using the law `|result| < M/2`
419
+ * exact dot-product and GEMM output-bound receipts
420
+ * `SignedSession` high-level API
327
421
  * 4-rail engine (`127 × 8191 × 65536 × 524287`)
328
422
  * AVX2-accelerated encoded kernels
329
423
  * fused `fma(...)`
@@ -86,12 +86,86 @@ hot = s.hot_loop_affine(x, multiplier=1_000_003, addend=7, iterations=1000)
86
86
 
87
87
  ---
88
88
 
89
+ ## Centered signed arithmetic and range certificates
90
+
91
+ The native core computes exact residues modulo `M`. Version 0.5 adds a canonical signed view over:
92
+
93
+ ```text
94
+ [-M/2, M/2 - 1]
95
+ =
96
+ [-17,871,445,090,598,912, 17,871,445,090,598,911]
97
+ ```
98
+
99
+ ```python
100
+ import numpy as np
101
+ import rns_engine as rns
102
+
103
+ values = np.array([-5, 0, 7], dtype=np.int64)
104
+ rails = rns.encode_signed(values)
105
+ round_trip = rns.decode_signed(*rails)
106
+
107
+ assert np.array_equal(round_trip, values)
108
+ ```
109
+
110
+ `encode_signed(...)` rejects values outside the centered interval instead of silently wrapping them modulo `M`.
111
+
112
+ A centered decoding is a canonical interpretation of a residue. It is **not by itself proof that an unknown mathematical result did not wrap**. Unique signed reconstruction requires an independent strict magnitude bound:
113
+
114
+ ```text
115
+ |result| < M/2
116
+ ```
117
+
118
+ Use a range certificate before claiming that a residue is the unique signed integer result:
119
+
120
+ ```python
121
+ certificate = rns.certify_signed_bound(max_abs_bound=1_000_000)
122
+ certificate.require_unique()
123
+
124
+ print(certificate.headroom)
125
+ print(certificate.minimum_required_modulus)
126
+ ```
127
+
128
+ For a signed dot product or one GEMM output entry, the engine can build the worst-case receipt directly:
129
+
130
+ ```python
131
+ certificate = rns.certify_signed_dot_bound(
132
+ inner_dimension=5120,
133
+ left_abs_bound=127,
134
+ right_abs_bound=127,
135
+ )
136
+
137
+ assert certificate.max_abs_bound == 82_580_480
138
+ assert certificate.unique
139
+ certificate.require_unique()
140
+ ```
141
+
142
+ This uses the exact conservative law:
143
+
144
+ ```text
145
+ output bound
146
+ =
147
+ inner_dimension × left bound × right bound + addend bound
148
+ ```
149
+
150
+ `SignedSession` exposes the existing residue arithmetic with signed external values:
151
+
152
+ ```python
153
+ s = rns.SignedSession()
154
+ a = s.encode_signed([-5, 7])
155
+ b = s.encode_signed([3, -10])
156
+ out = s.decode_signed(s.add(a, b))
157
+ ```
158
+
159
+ ---
160
+
89
161
  ## Core API
90
162
 
91
163
  ### Encoded rail API
92
164
 
93
165
  * `rns.encode(x)` → `(r0, r1, r2, r3)`
94
166
  * `rns.decode(r0, r1, r2, r3)` → `uint64[]`
167
+ * `rns.encode_signed(x)` → strict centered signed encoding
168
+ * `rns.decode_signed(r0, r1, r2, r3)` → canonical `int64[]`
95
169
  * `rns.add(*ea, *eb)`
96
170
  * `rns.sub(*ea, *eb)`
97
171
  * `rns.mul(*ea, *eb)`
@@ -99,6 +173,15 @@ hot = s.hot_loop_affine(x, multiplier=1_000_003, addend=7, iterations=1000)
99
173
  * `rns.fma(*ea, *eb, *ec)`
100
174
  * `rns.op(*ea, *eb, code)` where `0=add 1=mul 2=sub 3=div`
101
175
 
176
+ ### Range-certificate API
177
+
178
+ * `rns.certify_signed_bound(max_abs_bound)`
179
+ * `rns.certify_signed_dot_bound(inner_dimension, left_abs_bound, right_abs_bound, addend_abs_bound=0)`
180
+ * `SignedRangeCertificate.unique`
181
+ * `SignedRangeCertificate.headroom`
182
+ * `SignedRangeCertificate.minimum_required_modulus`
183
+ * `SignedRangeCertificate.require_unique()`
184
+
102
185
  ### Scalar-broadcast encoded API
103
186
 
104
187
  These avoid materializing full constant arrays in Python:
@@ -136,6 +219,7 @@ Auto-dispatch variants:
136
219
  ### High-level API
137
220
 
138
221
  * `rns.Session`
222
+ * `rns.SignedSession`
139
223
  * `rns.SessionCache`
140
224
  * `rns.EncodedArray`
141
225
 
@@ -185,7 +269,9 @@ def make_invertible_divisor(b):
185
269
  ## Data model
186
270
 
187
271
  * input arrays to `encode(...)` are treated as `uint64`
188
- * values outside `[0, M)` are reduced mod `M` during encode
272
+ * values outside `[0, M)` are reduced mod `M` during unsigned encode
273
+ * `encode_signed(...)` accepts only integers in `[-M/2, M/2 - 1]` and refuses silent wrapping
274
+ * `decode_signed(...)` returns the canonical centered `int64` view
189
275
  * rails are returned as:
190
276
  * `r0`: `uint16`
191
277
  * `r1`: `uint16`
@@ -228,7 +314,8 @@ Workload: `affine_repeat_u64_io(x, 1_000_003, 7, iterations=1000)`
228
314
  ### Verification status
229
315
 
230
316
  * correctness sanity checks passed
231
- * full test suite passed: **50 / 50**
317
+ * the full suite is exercised on Linux, macOS, and Windows across Python 3.10–3.13
318
+ * built wheels are installed and tested before release
232
319
 
233
320
  ---
234
321
 
@@ -287,6 +374,9 @@ import rns_engine as rns
287
374
  rns.info()
288
375
 
289
376
  rns.M
377
+ rns.HALF_M
378
+ rns.SIGNED_MIN
379
+ rns.SIGNED_MAX
290
380
  rns.M0
291
381
  rns.M1
292
382
  rns.M2
@@ -298,9 +388,13 @@ rns.HAS_AVX2
298
388
 
299
389
  ## Current release
300
390
 
301
- ### v0.4.1
391
+ ### v0.5.0
302
392
 
303
- * Maintenance release: repaired packaging metadata, cross-platform wheel builds, and Python 3.10–3.13 CI; arithmetic core unchanged
393
+ * canonical centered signed encoding and decoding
394
+ * strict signed input validation with no silent modular wrapping
395
+ * signed uniqueness certificates using the law `|result| < M/2`
396
+ * exact dot-product and GEMM output-bound receipts
397
+ * `SignedSession` high-level API
304
398
  * 4-rail engine (`127 × 8191 × 65536 × 524287`)
305
399
  * AVX2-accelerated encoded kernels
306
400
  * fused `fma(...)`
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "rns_engine"
7
- version = "0.4.1"
7
+ version = "0.5.0"
8
8
  description = "Exact RNS engine with session/cache API."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -39,6 +39,19 @@ from ._core import (
39
39
  omp_num_procs,
40
40
  )
41
41
  from .engine import EncodedArray, Session, SessionCache
42
+ from .signed import (
43
+ HALF_M,
44
+ SIGNED_MAX,
45
+ SIGNED_MIN,
46
+ UNIQUE_SIGNED_MAX,
47
+ UNIQUE_SIGNED_MIN,
48
+ SignedRangeCertificate,
49
+ SignedSession,
50
+ certify_signed_bound,
51
+ certify_signed_dot_bound,
52
+ decode_signed,
53
+ encode_signed,
54
+ )
42
55
 
43
56
  try:
44
57
  __version__ = _distribution_version("rns_engine")
@@ -50,7 +63,9 @@ except PackageNotFoundError:
50
63
  __all__ = [
51
64
  "HAS_AVX2",
52
65
  "M", "M0", "M1", "M2", "M3",
53
- "encode", "decode", "op",
66
+ "HALF_M", "SIGNED_MIN", "SIGNED_MAX",
67
+ "UNIQUE_SIGNED_MIN", "UNIQUE_SIGNED_MAX",
68
+ "encode", "decode", "encode_signed", "decode_signed", "op",
54
69
  "add", "sub", "mul", "div_", "fma",
55
70
  "affine_repeat",
56
71
  "mul_u64", "fma_u64", "affine_repeat_u64",
@@ -60,13 +75,17 @@ __all__ = [
60
75
  "fma_u64_io", "fma_u64_io_omp", "fma_u64_auto",
61
76
  "affine_repeat_u64_io", "affine_repeat_u64_io_omp", "affine_repeat_u64_auto",
62
77
  "omp_max_threads", "omp_set_num_threads", "omp_num_procs",
63
- "EncodedArray", "SessionCache", "Session",
78
+ "EncodedArray", "SessionCache", "Session", "SignedSession",
79
+ "SignedRangeCertificate", "certify_signed_bound", "certify_signed_dot_bound",
64
80
  "info",
65
81
  ]
66
82
 
83
+
67
84
  def info():
68
85
  print(f"rns_engine v{__version__}")
69
86
  print(f" Dynamic range : [0, {M:,})")
87
+ print(f" Signed view : [{SIGNED_MIN:,}, {SIGNED_MAX:,}]")
88
+ print(f" Unique bound : |x| < {HALF_M:,}")
70
89
  print(f" Moduli : {M0} x {M1} x {M2} x {M3}")
71
90
  print(f" AVX2 : {'yes' if HAS_AVX2 else 'no'}")
72
91
  print(" Core APIs : add/sub/mul/fma + raw/omp/auto scalar-broadcast family")
@@ -0,0 +1,211 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from operator import index
5
+ from typing import Any
6
+
7
+ import numpy as np
8
+
9
+ from ._core import M
10
+ from ._core import decode as _decode
11
+ from ._core import encode as _encode
12
+ from .engine import EncodedArray, Session
13
+
14
+ HALF_M = int(M) // 2
15
+ SIGNED_MIN = -HALF_M
16
+ SIGNED_MAX = HALF_M - 1
17
+ UNIQUE_SIGNED_MIN = -HALF_M + 1
18
+ UNIQUE_SIGNED_MAX = HALF_M - 1
19
+
20
+
21
+ def _require_nonnegative_integer(value: Any, *, name: str) -> int:
22
+ try:
23
+ integer = index(value)
24
+ except TypeError as exc:
25
+ raise TypeError(f"{name} must be an integer") from exc
26
+ if integer < 0:
27
+ raise ValueError(f"{name} must be >= 0")
28
+ return integer
29
+
30
+
31
+ def _as_signed_int64_array(x: Any) -> np.ndarray:
32
+ raw = np.asarray(x)
33
+ if raw.ndim != 1:
34
+ raise ValueError("expected a 1D array-like of signed integer values")
35
+
36
+ if np.issubdtype(raw.dtype, np.integer):
37
+ if np.issubdtype(raw.dtype, np.unsignedinteger):
38
+ if raw.size and np.any(raw > np.uint64(SIGNED_MAX)):
39
+ raise OverflowError(
40
+ f"signed RNS input must be in [{SIGNED_MIN}, {SIGNED_MAX}]"
41
+ )
42
+ else:
43
+ if raw.size and (
44
+ np.any(raw < np.int64(SIGNED_MIN))
45
+ or np.any(raw > np.int64(SIGNED_MAX))
46
+ ):
47
+ raise OverflowError(
48
+ f"signed RNS input must be in [{SIGNED_MIN}, {SIGNED_MAX}]"
49
+ )
50
+ return np.ascontiguousarray(raw, dtype=np.int64)
51
+
52
+ if raw.dtype == object:
53
+ values: list[int] = []
54
+ for position, value in enumerate(raw):
55
+ try:
56
+ integer = index(value)
57
+ except TypeError as exc:
58
+ raise TypeError(
59
+ f"signed RNS input at index {position} is not an integer"
60
+ ) from exc
61
+ if integer < SIGNED_MIN or integer > SIGNED_MAX:
62
+ raise OverflowError(
63
+ f"signed RNS input at index {position} must be in "
64
+ f"[{SIGNED_MIN}, {SIGNED_MAX}]"
65
+ )
66
+ values.append(integer)
67
+ return np.ascontiguousarray(values, dtype=np.int64)
68
+
69
+ raise TypeError("signed RNS input must contain integers")
70
+
71
+
72
+ def encode_signed(x: Any) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
73
+ """Encode canonical centered integers without silent modular wrapping.
74
+
75
+ Accepted values are in ``[-M/2, M/2 - 1]``. Negative values are mapped to
76
+ their canonical residues before entering the native four-rail encoder.
77
+ """
78
+
79
+ signed = _as_signed_int64_array(x)
80
+ residues = signed.copy()
81
+ negative = residues < 0
82
+ residues[negative] += int(M)
83
+ return _encode(np.ascontiguousarray(residues, dtype=np.uint64))
84
+
85
+
86
+ def decode_signed(
87
+ r0: Any,
88
+ r1: Any,
89
+ r2: Any,
90
+ r3: Any,
91
+ ) -> np.ndarray:
92
+ """Decode residues into the canonical centered interval.
93
+
94
+ The residue ``M/2`` is represented as ``-M/2``. This is a canonical
95
+ interpretation, not proof that an unknown mathematical result did not wrap;
96
+ use :func:`certify_signed_bound` for that guarantee.
97
+ """
98
+
99
+ residues = _decode(r0, r1, r2, r3)
100
+ centered = np.ascontiguousarray(residues, dtype=np.int64)
101
+ upper_half = residues >= np.uint64(HALF_M)
102
+ centered[upper_half] -= int(M)
103
+ return centered
104
+
105
+
106
+ @dataclass(frozen=True, slots=True)
107
+ class SignedRangeCertificate:
108
+ """Certificate that a known absolute bound has one signed RNS decoding."""
109
+
110
+ max_abs_bound: int
111
+
112
+ def __post_init__(self) -> None:
113
+ bound = _require_nonnegative_integer(
114
+ self.max_abs_bound,
115
+ name="max_abs_bound",
116
+ )
117
+ object.__setattr__(self, "max_abs_bound", bound)
118
+
119
+ @property
120
+ def modulus(self) -> int:
121
+ return int(M)
122
+
123
+ @property
124
+ def half_modulus(self) -> int:
125
+ return HALF_M
126
+
127
+ @property
128
+ def unique(self) -> bool:
129
+ return self.max_abs_bound < HALF_M
130
+
131
+ @property
132
+ def headroom(self) -> int:
133
+ """Remaining strict signed headroom; negative means the bound exceeds it."""
134
+
135
+ return UNIQUE_SIGNED_MAX - self.max_abs_bound
136
+
137
+ @property
138
+ def minimum_required_modulus(self) -> int:
139
+ """Smallest modulus that uniquely supports ``[-bound, +bound]``."""
140
+
141
+ return 2 * self.max_abs_bound + 1
142
+
143
+ def require_unique(self) -> SignedRangeCertificate:
144
+ if not self.unique:
145
+ raise OverflowError(
146
+ "signed result is modular-only: the certified absolute bound "
147
+ f"{self.max_abs_bound} must be less than M/2 ({HALF_M})"
148
+ )
149
+ return self
150
+
151
+
152
+ def certify_signed_bound(max_abs_bound: int) -> SignedRangeCertificate:
153
+ """Return a strict uniqueness certificate for a known absolute bound."""
154
+
155
+ return SignedRangeCertificate(max_abs_bound=max_abs_bound)
156
+
157
+
158
+ def certify_signed_dot_bound(
159
+ inner_dimension: int,
160
+ left_abs_bound: int,
161
+ right_abs_bound: int,
162
+ addend_abs_bound: int = 0,
163
+ ) -> SignedRangeCertificate:
164
+ """Certify the worst-case magnitude of an exact signed dot product.
165
+
166
+ For ``sum(a[k] * b[k]) + c`` with ``inner_dimension`` terms, this uses the
167
+ conservative exact bound::
168
+
169
+ inner_dimension * left_abs_bound * right_abs_bound + addend_abs_bound
170
+
171
+ This is also the per-output bound for a GEMM whose entries satisfy the same
172
+ magnitude limits. The returned certificate is unique only when that bound
173
+ is strictly less than ``M/2``.
174
+ """
175
+
176
+ k = _require_nonnegative_integer(inner_dimension, name="inner_dimension")
177
+ left = _require_nonnegative_integer(left_abs_bound, name="left_abs_bound")
178
+ right = _require_nonnegative_integer(
179
+ right_abs_bound,
180
+ name="right_abs_bound",
181
+ )
182
+ addend = _require_nonnegative_integer(
183
+ addend_abs_bound,
184
+ name="addend_abs_bound",
185
+ )
186
+ return certify_signed_bound(k * left * right + addend)
187
+
188
+
189
+ class SignedSession(Session):
190
+ """Session API whose external values use centered signed RNS semantics."""
191
+
192
+ def encode_signed(self, x: Any) -> EncodedArray:
193
+ return EncodedArray(*encode_signed(x))
194
+
195
+ def decode_signed(self, x: EncodedArray) -> np.ndarray:
196
+ return decode_signed(*x.rails())
197
+
198
+
199
+ __all__ = [
200
+ "HALF_M",
201
+ "SIGNED_MIN",
202
+ "SIGNED_MAX",
203
+ "UNIQUE_SIGNED_MIN",
204
+ "UNIQUE_SIGNED_MAX",
205
+ "SignedRangeCertificate",
206
+ "SignedSession",
207
+ "certify_signed_bound",
208
+ "certify_signed_dot_bound",
209
+ "encode_signed",
210
+ "decode_signed",
211
+ ]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rns_engine
3
- Version: 0.4.1
3
+ Version: 0.5.0
4
4
  Summary: Exact RNS engine with session/cache API.
5
5
  Author: Evan Wesley
6
6
  License-Expression: AGPL-3.0-only
@@ -109,12 +109,86 @@ hot = s.hot_loop_affine(x, multiplier=1_000_003, addend=7, iterations=1000)
109
109
 
110
110
  ---
111
111
 
112
+ ## Centered signed arithmetic and range certificates
113
+
114
+ The native core computes exact residues modulo `M`. Version 0.5 adds a canonical signed view over:
115
+
116
+ ```text
117
+ [-M/2, M/2 - 1]
118
+ =
119
+ [-17,871,445,090,598,912, 17,871,445,090,598,911]
120
+ ```
121
+
122
+ ```python
123
+ import numpy as np
124
+ import rns_engine as rns
125
+
126
+ values = np.array([-5, 0, 7], dtype=np.int64)
127
+ rails = rns.encode_signed(values)
128
+ round_trip = rns.decode_signed(*rails)
129
+
130
+ assert np.array_equal(round_trip, values)
131
+ ```
132
+
133
+ `encode_signed(...)` rejects values outside the centered interval instead of silently wrapping them modulo `M`.
134
+
135
+ A centered decoding is a canonical interpretation of a residue. It is **not by itself proof that an unknown mathematical result did not wrap**. Unique signed reconstruction requires an independent strict magnitude bound:
136
+
137
+ ```text
138
+ |result| < M/2
139
+ ```
140
+
141
+ Use a range certificate before claiming that a residue is the unique signed integer result:
142
+
143
+ ```python
144
+ certificate = rns.certify_signed_bound(max_abs_bound=1_000_000)
145
+ certificate.require_unique()
146
+
147
+ print(certificate.headroom)
148
+ print(certificate.minimum_required_modulus)
149
+ ```
150
+
151
+ For a signed dot product or one GEMM output entry, the engine can build the worst-case receipt directly:
152
+
153
+ ```python
154
+ certificate = rns.certify_signed_dot_bound(
155
+ inner_dimension=5120,
156
+ left_abs_bound=127,
157
+ right_abs_bound=127,
158
+ )
159
+
160
+ assert certificate.max_abs_bound == 82_580_480
161
+ assert certificate.unique
162
+ certificate.require_unique()
163
+ ```
164
+
165
+ This uses the exact conservative law:
166
+
167
+ ```text
168
+ output bound
169
+ =
170
+ inner_dimension × left bound × right bound + addend bound
171
+ ```
172
+
173
+ `SignedSession` exposes the existing residue arithmetic with signed external values:
174
+
175
+ ```python
176
+ s = rns.SignedSession()
177
+ a = s.encode_signed([-5, 7])
178
+ b = s.encode_signed([3, -10])
179
+ out = s.decode_signed(s.add(a, b))
180
+ ```
181
+
182
+ ---
183
+
112
184
  ## Core API
113
185
 
114
186
  ### Encoded rail API
115
187
 
116
188
  * `rns.encode(x)` → `(r0, r1, r2, r3)`
117
189
  * `rns.decode(r0, r1, r2, r3)` → `uint64[]`
190
+ * `rns.encode_signed(x)` → strict centered signed encoding
191
+ * `rns.decode_signed(r0, r1, r2, r3)` → canonical `int64[]`
118
192
  * `rns.add(*ea, *eb)`
119
193
  * `rns.sub(*ea, *eb)`
120
194
  * `rns.mul(*ea, *eb)`
@@ -122,6 +196,15 @@ hot = s.hot_loop_affine(x, multiplier=1_000_003, addend=7, iterations=1000)
122
196
  * `rns.fma(*ea, *eb, *ec)`
123
197
  * `rns.op(*ea, *eb, code)` where `0=add 1=mul 2=sub 3=div`
124
198
 
199
+ ### Range-certificate API
200
+
201
+ * `rns.certify_signed_bound(max_abs_bound)`
202
+ * `rns.certify_signed_dot_bound(inner_dimension, left_abs_bound, right_abs_bound, addend_abs_bound=0)`
203
+ * `SignedRangeCertificate.unique`
204
+ * `SignedRangeCertificate.headroom`
205
+ * `SignedRangeCertificate.minimum_required_modulus`
206
+ * `SignedRangeCertificate.require_unique()`
207
+
125
208
  ### Scalar-broadcast encoded API
126
209
 
127
210
  These avoid materializing full constant arrays in Python:
@@ -159,6 +242,7 @@ Auto-dispatch variants:
159
242
  ### High-level API
160
243
 
161
244
  * `rns.Session`
245
+ * `rns.SignedSession`
162
246
  * `rns.SessionCache`
163
247
  * `rns.EncodedArray`
164
248
 
@@ -208,7 +292,9 @@ def make_invertible_divisor(b):
208
292
  ## Data model
209
293
 
210
294
  * input arrays to `encode(...)` are treated as `uint64`
211
- * values outside `[0, M)` are reduced mod `M` during encode
295
+ * values outside `[0, M)` are reduced mod `M` during unsigned encode
296
+ * `encode_signed(...)` accepts only integers in `[-M/2, M/2 - 1]` and refuses silent wrapping
297
+ * `decode_signed(...)` returns the canonical centered `int64` view
212
298
  * rails are returned as:
213
299
  * `r0`: `uint16`
214
300
  * `r1`: `uint16`
@@ -251,7 +337,8 @@ Workload: `affine_repeat_u64_io(x, 1_000_003, 7, iterations=1000)`
251
337
  ### Verification status
252
338
 
253
339
  * correctness sanity checks passed
254
- * full test suite passed: **50 / 50**
340
+ * the full suite is exercised on Linux, macOS, and Windows across Python 3.10–3.13
341
+ * built wheels are installed and tested before release
255
342
 
256
343
  ---
257
344
 
@@ -310,6 +397,9 @@ import rns_engine as rns
310
397
  rns.info()
311
398
 
312
399
  rns.M
400
+ rns.HALF_M
401
+ rns.SIGNED_MIN
402
+ rns.SIGNED_MAX
313
403
  rns.M0
314
404
  rns.M1
315
405
  rns.M2
@@ -321,9 +411,13 @@ rns.HAS_AVX2
321
411
 
322
412
  ## Current release
323
413
 
324
- ### v0.4.1
414
+ ### v0.5.0
325
415
 
326
- * Maintenance release: repaired packaging metadata, cross-platform wheel builds, and Python 3.10–3.13 CI; arithmetic core unchanged
416
+ * canonical centered signed encoding and decoding
417
+ * strict signed input validation with no silent modular wrapping
418
+ * signed uniqueness certificates using the law `|result| < M/2`
419
+ * exact dot-product and GEMM output-bound receipts
420
+ * `SignedSession` high-level API
327
421
  * 4-rail engine (`127 × 8191 × 65536 × 524287`)
328
422
  * AVX2-accelerated encoded kernels
329
423
  * fused `fma(...)`
@@ -5,6 +5,7 @@ setup.py
5
5
  src/rns_engine/__init__.py
6
6
  src/rns_engine/_core.cpp
7
7
  src/rns_engine/engine.py
8
+ src/rns_engine/signed.py
8
9
  src/rns_engine.egg-info/PKG-INFO
9
10
  src/rns_engine.egg-info/SOURCES.txt
10
11
  src/rns_engine.egg-info/dependency_links.txt
@@ -13,4 +14,5 @@ src/rns_engine.egg-info/requires.txt
13
14
  src/rns_engine.egg-info/top_level.txt
14
15
  tests/test_packaging.py
15
16
  tests/test_rns.py
16
- tests/test_session.py
17
+ tests/test_session.py
18
+ tests/test_signed.py
@@ -0,0 +1,138 @@
1
+ import numpy as np
2
+ import pytest
3
+
4
+ import rns_engine as rns
5
+
6
+
7
+ def test_signed_constants_match_four_rail_modulus():
8
+ assert rns.M == 35_742_890_181_197_824
9
+ assert rns.HALF_M == rns.M // 2
10
+ assert rns.SIGNED_MIN == -rns.HALF_M
11
+ assert rns.SIGNED_MAX == rns.HALF_M - 1
12
+ assert rns.UNIQUE_SIGNED_MIN == -rns.HALF_M + 1
13
+ assert rns.UNIQUE_SIGNED_MAX == rns.HALF_M - 1
14
+
15
+
16
+ def test_signed_round_trip_including_canonical_boundaries():
17
+ values = np.array(
18
+ [rns.SIGNED_MIN, -1_000_003, -1, 0, 1, 1_000_003, rns.SIGNED_MAX],
19
+ dtype=np.int64,
20
+ )
21
+
22
+ decoded = rns.decode_signed(*rns.encode_signed(values))
23
+
24
+ np.testing.assert_array_equal(decoded, values)
25
+ assert decoded.dtype == np.int64
26
+
27
+
28
+ def test_negative_one_uses_the_same_residue_as_m_minus_one():
29
+ signed_rails = rns.encode_signed(np.array([-1], dtype=np.int64))
30
+ unsigned_rails = rns.encode(np.array([rns.M - 1], dtype=np.uint64))
31
+
32
+ for signed_rail, unsigned_rail in zip(signed_rails, unsigned_rails):
33
+ np.testing.assert_array_equal(signed_rail, unsigned_rail)
34
+
35
+
36
+ def test_half_modulus_residue_has_canonical_negative_interpretation():
37
+ rails = rns.encode(np.array([rns.HALF_M], dtype=np.uint64))
38
+
39
+ decoded = rns.decode_signed(*rails)
40
+
41
+ np.testing.assert_array_equal(decoded, np.array([rns.SIGNED_MIN], dtype=np.int64))
42
+
43
+
44
+ def test_signed_encoder_refuses_silent_wraparound():
45
+ with pytest.raises(OverflowError, match="signed RNS input"):
46
+ rns.encode_signed([rns.SIGNED_MIN - 1])
47
+
48
+ with pytest.raises(OverflowError, match="signed RNS input"):
49
+ rns.encode_signed([rns.SIGNED_MAX + 1])
50
+
51
+
52
+ def test_signed_encoder_rejects_non_integer_and_non_vector_input():
53
+ with pytest.raises(TypeError, match="must contain integers"):
54
+ rns.encode_signed([1.5, 2.5])
55
+
56
+ with pytest.raises(ValueError, match="1D"):
57
+ rns.encode_signed([[1, 2], [3, 4]])
58
+
59
+
60
+ def test_signed_session_uses_existing_residue_arithmetic():
61
+ session = rns.SignedSession()
62
+ a = session.encode_signed([-5, 7])
63
+ b = session.encode_signed([3, -10])
64
+
65
+ np.testing.assert_array_equal(
66
+ session.decode_signed(session.add(a, b)),
67
+ np.array([-2, -3], dtype=np.int64),
68
+ )
69
+ np.testing.assert_array_equal(
70
+ session.decode_signed(session.mul(a, b)),
71
+ np.array([-15, -70], dtype=np.int64),
72
+ )
73
+
74
+
75
+ def test_strict_signed_bound_certificate():
76
+ safe = rns.certify_signed_bound(rns.HALF_M - 1)
77
+ assert safe.unique is True
78
+ assert safe.headroom == 0
79
+ assert safe.minimum_required_modulus == rns.M - 1
80
+ assert safe.require_unique() is safe
81
+
82
+ ambiguous = rns.certify_signed_bound(rns.HALF_M)
83
+ assert ambiguous.unique is False
84
+ assert ambiguous.headroom == -1
85
+ assert ambiguous.minimum_required_modulus == rns.M + 1
86
+ with pytest.raises(OverflowError, match="modular-only"):
87
+ ambiguous.require_unique()
88
+
89
+
90
+ def test_signed_bound_certificate_validates_the_bound():
91
+ with pytest.raises(ValueError, match=">= 0"):
92
+ rns.certify_signed_bound(-1)
93
+
94
+ with pytest.raises(TypeError, match="must be an integer"):
95
+ rns.certify_signed_bound(1.5)
96
+
97
+
98
+ def test_warpfrac_int8_dot_bound_is_uniquely_signed():
99
+ certificate = rns.certify_signed_dot_bound(
100
+ inner_dimension=5120,
101
+ left_abs_bound=127,
102
+ right_abs_bound=127,
103
+ )
104
+
105
+ assert certificate.max_abs_bound == 82_580_480
106
+ assert certificate.unique is True
107
+ assert certificate.headroom == rns.UNIQUE_SIGNED_MAX - 82_580_480
108
+ assert certificate.require_unique() is certificate
109
+
110
+
111
+ def test_dot_bound_accounts_for_an_existing_addend():
112
+ certificate = rns.certify_signed_dot_bound(3, 5, 7, addend_abs_bound=11)
113
+
114
+ assert certificate.max_abs_bound == 116
115
+ assert certificate.minimum_required_modulus == 233
116
+
117
+
118
+ def test_dot_bound_exposes_modular_only_outputs():
119
+ certificate = rns.certify_signed_dot_bound(rns.HALF_M, 1, 1)
120
+
121
+ assert certificate.max_abs_bound == rns.HALF_M
122
+ assert certificate.unique is False
123
+ with pytest.raises(OverflowError, match="modular-only"):
124
+ certificate.require_unique()
125
+
126
+
127
+ def test_dot_bound_validates_each_input():
128
+ with pytest.raises(ValueError, match="inner_dimension"):
129
+ rns.certify_signed_dot_bound(-1, 1, 1)
130
+
131
+ with pytest.raises(TypeError, match="left_abs_bound"):
132
+ rns.certify_signed_dot_bound(1, 1.5, 1)
133
+
134
+ with pytest.raises(ValueError, match="right_abs_bound"):
135
+ rns.certify_signed_dot_bound(1, 1, -1)
136
+
137
+ with pytest.raises(TypeError, match="addend_abs_bound"):
138
+ rns.certify_signed_dot_bound(1, 1, 1, addend_abs_bound="7")
File without changes
File without changes
File without changes
File without changes