rns-engine 0.7.0__tar.gz → 0.8.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.
Files changed (28) hide show
  1. {rns_engine-0.7.0/src/rns_engine.egg-info → rns_engine-0.8.0}/PKG-INFO +60 -10
  2. {rns_engine-0.7.0 → rns_engine-0.8.0}/README.md +59 -9
  3. {rns_engine-0.7.0 → rns_engine-0.8.0}/pyproject.toml +1 -1
  4. {rns_engine-0.7.0 → rns_engine-0.8.0}/src/rns_engine/__init__.py +16 -0
  5. rns_engine-0.8.0/src/rns_engine/capacity.py +390 -0
  6. rns_engine-0.8.0/src/rns_engine/coefficients.py +179 -0
  7. {rns_engine-0.7.0 → rns_engine-0.8.0/src/rns_engine.egg-info}/PKG-INFO +60 -10
  8. {rns_engine-0.7.0 → rns_engine-0.8.0}/src/rns_engine.egg-info/SOURCES.txt +4 -0
  9. rns_engine-0.8.0/tests/test_capacity.py +161 -0
  10. rns_engine-0.8.0/tests/test_coefficients.py +110 -0
  11. {rns_engine-0.7.0 → rns_engine-0.8.0}/LICENSE +0 -0
  12. {rns_engine-0.7.0 → rns_engine-0.8.0}/setup.cfg +0 -0
  13. {rns_engine-0.7.0 → rns_engine-0.8.0}/setup.py +0 -0
  14. {rns_engine-0.7.0 → rns_engine-0.8.0}/src/rns_engine/_core.cpp +0 -0
  15. {rns_engine-0.7.0 → rns_engine-0.8.0}/src/rns_engine/_weighted.cpp +0 -0
  16. {rns_engine-0.7.0 → rns_engine-0.8.0}/src/rns_engine/engine.py +0 -0
  17. {rns_engine-0.7.0 → rns_engine-0.8.0}/src/rns_engine/signed.py +0 -0
  18. {rns_engine-0.7.0 → rns_engine-0.8.0}/src/rns_engine/weighted.py +0 -0
  19. {rns_engine-0.7.0 → rns_engine-0.8.0}/src/rns_engine.egg-info/dependency_links.txt +0 -0
  20. {rns_engine-0.7.0 → rns_engine-0.8.0}/src/rns_engine.egg-info/not-zip-safe +0 -0
  21. {rns_engine-0.7.0 → rns_engine-0.8.0}/src/rns_engine.egg-info/requires.txt +0 -0
  22. {rns_engine-0.7.0 → rns_engine-0.8.0}/src/rns_engine.egg-info/top_level.txt +0 -0
  23. {rns_engine-0.7.0 → rns_engine-0.8.0}/tests/test_packaging.py +0 -0
  24. {rns_engine-0.7.0 → rns_engine-0.8.0}/tests/test_rns.py +0 -0
  25. {rns_engine-0.7.0 → rns_engine-0.8.0}/tests/test_session.py +0 -0
  26. {rns_engine-0.7.0 → rns_engine-0.8.0}/tests/test_signed.py +0 -0
  27. {rns_engine-0.7.0 → rns_engine-0.8.0}/tests/test_weighted.py +0 -0
  28. {rns_engine-0.7.0 → rns_engine-0.8.0}/tests/test_weighted_benchmark.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rns_engine
3
- Version: 0.7.0
3
+ Version: 0.8.0
4
4
  Summary: Exact RNS engine with session/cache API.
5
5
  Author: Evan Wesley
6
6
  License-Expression: AGPL-3.0-only
@@ -194,7 +194,7 @@ out = session.decode_signed(session.add(a, b))
194
194
 
195
195
  ## Weighted INT32 partial accumulation
196
196
 
197
- Version 0.7 provides the fused direct bridge for digit-plane, chunked-GEMM, and Tensor Core pipelines.
197
+ Version 0.7 introduced the fused direct bridge for digit-plane, chunked-GEMM, and Tensor Core pipelines.
198
198
 
199
199
  Give the engine signed INT32 partial outputs with shape:
200
200
 
@@ -246,7 +246,7 @@ Important details:
246
246
  - `decode_modular()` always returns the canonical residue in `[0, M)`.
247
247
  - Empty term sets produce an exact zero result.
248
248
 
249
- The current implementation uses one fused native `_weighted` call to read signed INT32 partials, collect exact per-term magnitude bounds, apply positional weights modulo `M`, and accumulate all four rails. The pre-fusion staged encode → scale → add body remains available internally as an exact reference for A/B tests. Local AVX2 diagnostics measured roughly 2.1×–2.9× speedup at one million outputs and larger gains on tiny many-term cases; those figures are hardware-specific CPU evidence, not CUDA, Tensor Core, or universal performance claims.
249
+ The implementation uses one fused native `_weighted` call to read signed INT32 partials, collect exact per-term magnitude bounds, apply positional weights modulo `M`, and accumulate all four rails. The pre-fusion staged encode → scale → add body remains available internally as an exact reference for A/B tests. Local AVX2 diagnostics measured roughly 2.1×–2.9× speedup at one million outputs and larger gains on tiny many-term cases; those figures are hardware-specific CPU evidence, not CUDA, Tensor Core, or universal performance claims.
250
250
 
251
251
  A precomputed receipt can also be created without materializing partial arrays:
252
252
 
@@ -260,6 +260,45 @@ certificate.require_unique()
260
260
 
261
261
  ---
262
262
 
263
+ ## Exact GEMM capacity planning
264
+
265
+ Version 0.8 separates two different safety questions that must not be conflated:
266
+
267
+ 1. Does each grouped raw digit-plane coefficient fit the native signed accumulator?
268
+ 2. Does the final radix-weighted result fit uniquely inside the global RNS modulus product?
269
+
270
+ ```python
271
+ import rns_engine as rns
272
+
273
+ local = rns.plan_grouped_coefficient_capacity(
274
+ inner_dimension=5120,
275
+ left_digit_abs_bounds=[127] * 8,
276
+ right_digit_abs_bounds=[127] * 8,
277
+ accumulator_bits=32,
278
+ )
279
+
280
+ global_plan = rns.plan_digit_plane_gemm_capacity(
281
+ inner_dimension=5120,
282
+ radix=128,
283
+ left_digit_abs_bounds=[127] * 8,
284
+ right_digit_abs_bounds=[127] * 8,
285
+ )
286
+
287
+ assert local.max_abs_bound == 660_643_840
288
+ assert local.safe
289
+ assert local.minimum_signed_accumulator_bits == 31
290
+
291
+ assert global_plan.capacity.required_modulus_bits == 126
292
+ assert global_plan.capacity.additional_bits_required == 71
293
+ assert not global_plan.current_unique
294
+ ```
295
+
296
+ That workload is locally safe in INT32 but globally modular-only in the current four-rail range. The planner can also validate explicit proposed CRT moduli and rejects candidates that are not pairwise coprime with the existing product.
297
+
298
+ See [`CAPACITY_PLANNING.md`](CAPACITY_PLANNING.md) for the exact laws, examples, and limits of what the receipts prove.
299
+
300
+ ---
301
+
263
302
  ## Core API
264
303
 
265
304
  ### Rail operations
@@ -285,6 +324,16 @@ certificate.require_unique()
285
324
  - `WeightedInt32Result.decode_signed(require_unique=True)`
286
325
  - `WeightedInt32Result.decode_modular()`
287
326
 
327
+ ### Capacity planning
328
+
329
+ - `plan_signed_capacity(max_abs_bound, additional_moduli=())`
330
+ - `plan_weighted_sum_capacity(weights, term_abs_bounds, addend_abs_bound=0, additional_moduli=())`
331
+ - `plan_digit_plane_gemm_capacity(inner_dimension, radix, left_digit_abs_bounds, right_digit_abs_bounds, ...)`
332
+ - `plan_grouped_coefficient_capacity(inner_dimension, left_digit_abs_bounds, right_digit_abs_bounds, accumulator_bits=32)`
333
+ - `SignedCapacityPlan`
334
+ - `DigitPlaneGemmCapacityPlan`
335
+ - `GroupedCoefficientCapacityPlan`
336
+
288
337
  ### Scalar-broadcast encoded operations
289
338
 
290
339
  - `mul_u64(...)`
@@ -359,7 +408,7 @@ Workload: `affine_repeat_u64_io(x, 1_000_003, 7, iterations=1000)`
359
408
  - OpenMP with one thread: **82.80 billion ops/sec**
360
409
  - OpenMP with two threads: **94.86 billion ops/sec**
361
410
 
362
- These measurements cover the existing affine kernels, not the new weighted INT32 orchestration path.
411
+ These measurements cover the existing affine kernels, not weighted INT32 reconstruction or the pure-Python capacity planners.
363
412
 
364
413
  ---
365
414
 
@@ -404,14 +453,15 @@ rns.HAS_AVX2
404
453
 
405
454
  ## Current release
406
455
 
407
- ### v0.7.0
456
+ ### v0.8.0
408
457
 
458
+ - exact local grouped-coefficient accumulator planning
459
+ - exact global digit-plane GEMM RNS-capacity planning
460
+ - strict separation between local INT32 safety and global signed uniqueness
461
+ - candidate CRT-modulus validation with pairwise-coprime enforcement
462
+ - exact modulus shortfall and additional-bit receipts
409
463
  - fused native weighted signed INT32 partial accumulation
410
- - one compiled pass for magnitude collection, positional weighting, and four-rail accumulation
411
- - staged reference body retained for exact fused-versus-staged A/B benchmarking
412
- - exact arbitrary-precision positional weights
413
- - per-term magnitude receipts and strict no-wrap certification
414
- - canonical modular and uniqueness-gated signed decode paths
464
+ - arbitrary-precision positional weights and no-wrap certificates
415
465
  - canonical centered signed encoding and decoding
416
466
  - exact dot-product and GEMM output-bound receipts
417
467
  - `SignedSession` high-level API
@@ -171,7 +171,7 @@ out = session.decode_signed(session.add(a, b))
171
171
 
172
172
  ## Weighted INT32 partial accumulation
173
173
 
174
- Version 0.7 provides the fused direct bridge for digit-plane, chunked-GEMM, and Tensor Core pipelines.
174
+ Version 0.7 introduced the fused direct bridge for digit-plane, chunked-GEMM, and Tensor Core pipelines.
175
175
 
176
176
  Give the engine signed INT32 partial outputs with shape:
177
177
 
@@ -223,7 +223,7 @@ Important details:
223
223
  - `decode_modular()` always returns the canonical residue in `[0, M)`.
224
224
  - Empty term sets produce an exact zero result.
225
225
 
226
- The current implementation uses one fused native `_weighted` call to read signed INT32 partials, collect exact per-term magnitude bounds, apply positional weights modulo `M`, and accumulate all four rails. The pre-fusion staged encode → scale → add body remains available internally as an exact reference for A/B tests. Local AVX2 diagnostics measured roughly 2.1×–2.9× speedup at one million outputs and larger gains on tiny many-term cases; those figures are hardware-specific CPU evidence, not CUDA, Tensor Core, or universal performance claims.
226
+ The implementation uses one fused native `_weighted` call to read signed INT32 partials, collect exact per-term magnitude bounds, apply positional weights modulo `M`, and accumulate all four rails. The pre-fusion staged encode → scale → add body remains available internally as an exact reference for A/B tests. Local AVX2 diagnostics measured roughly 2.1×–2.9× speedup at one million outputs and larger gains on tiny many-term cases; those figures are hardware-specific CPU evidence, not CUDA, Tensor Core, or universal performance claims.
227
227
 
228
228
  A precomputed receipt can also be created without materializing partial arrays:
229
229
 
@@ -237,6 +237,45 @@ certificate.require_unique()
237
237
 
238
238
  ---
239
239
 
240
+ ## Exact GEMM capacity planning
241
+
242
+ Version 0.8 separates two different safety questions that must not be conflated:
243
+
244
+ 1. Does each grouped raw digit-plane coefficient fit the native signed accumulator?
245
+ 2. Does the final radix-weighted result fit uniquely inside the global RNS modulus product?
246
+
247
+ ```python
248
+ import rns_engine as rns
249
+
250
+ local = rns.plan_grouped_coefficient_capacity(
251
+ inner_dimension=5120,
252
+ left_digit_abs_bounds=[127] * 8,
253
+ right_digit_abs_bounds=[127] * 8,
254
+ accumulator_bits=32,
255
+ )
256
+
257
+ global_plan = rns.plan_digit_plane_gemm_capacity(
258
+ inner_dimension=5120,
259
+ radix=128,
260
+ left_digit_abs_bounds=[127] * 8,
261
+ right_digit_abs_bounds=[127] * 8,
262
+ )
263
+
264
+ assert local.max_abs_bound == 660_643_840
265
+ assert local.safe
266
+ assert local.minimum_signed_accumulator_bits == 31
267
+
268
+ assert global_plan.capacity.required_modulus_bits == 126
269
+ assert global_plan.capacity.additional_bits_required == 71
270
+ assert not global_plan.current_unique
271
+ ```
272
+
273
+ That workload is locally safe in INT32 but globally modular-only in the current four-rail range. The planner can also validate explicit proposed CRT moduli and rejects candidates that are not pairwise coprime with the existing product.
274
+
275
+ See [`CAPACITY_PLANNING.md`](CAPACITY_PLANNING.md) for the exact laws, examples, and limits of what the receipts prove.
276
+
277
+ ---
278
+
240
279
  ## Core API
241
280
 
242
281
  ### Rail operations
@@ -262,6 +301,16 @@ certificate.require_unique()
262
301
  - `WeightedInt32Result.decode_signed(require_unique=True)`
263
302
  - `WeightedInt32Result.decode_modular()`
264
303
 
304
+ ### Capacity planning
305
+
306
+ - `plan_signed_capacity(max_abs_bound, additional_moduli=())`
307
+ - `plan_weighted_sum_capacity(weights, term_abs_bounds, addend_abs_bound=0, additional_moduli=())`
308
+ - `plan_digit_plane_gemm_capacity(inner_dimension, radix, left_digit_abs_bounds, right_digit_abs_bounds, ...)`
309
+ - `plan_grouped_coefficient_capacity(inner_dimension, left_digit_abs_bounds, right_digit_abs_bounds, accumulator_bits=32)`
310
+ - `SignedCapacityPlan`
311
+ - `DigitPlaneGemmCapacityPlan`
312
+ - `GroupedCoefficientCapacityPlan`
313
+
265
314
  ### Scalar-broadcast encoded operations
266
315
 
267
316
  - `mul_u64(...)`
@@ -336,7 +385,7 @@ Workload: `affine_repeat_u64_io(x, 1_000_003, 7, iterations=1000)`
336
385
  - OpenMP with one thread: **82.80 billion ops/sec**
337
386
  - OpenMP with two threads: **94.86 billion ops/sec**
338
387
 
339
- These measurements cover the existing affine kernels, not the new weighted INT32 orchestration path.
388
+ These measurements cover the existing affine kernels, not weighted INT32 reconstruction or the pure-Python capacity planners.
340
389
 
341
390
  ---
342
391
 
@@ -381,14 +430,15 @@ rns.HAS_AVX2
381
430
 
382
431
  ## Current release
383
432
 
384
- ### v0.7.0
433
+ ### v0.8.0
385
434
 
435
+ - exact local grouped-coefficient accumulator planning
436
+ - exact global digit-plane GEMM RNS-capacity planning
437
+ - strict separation between local INT32 safety and global signed uniqueness
438
+ - candidate CRT-modulus validation with pairwise-coprime enforcement
439
+ - exact modulus shortfall and additional-bit receipts
386
440
  - fused native weighted signed INT32 partial accumulation
387
- - one compiled pass for magnitude collection, positional weighting, and four-rail accumulation
388
- - staged reference body retained for exact fused-versus-staged A/B benchmarking
389
- - exact arbitrary-precision positional weights
390
- - per-term magnitude receipts and strict no-wrap certification
391
- - canonical modular and uniqueness-gated signed decode paths
441
+ - arbitrary-precision positional weights and no-wrap certificates
392
442
  - canonical centered signed encoding and decoding
393
443
  - exact dot-product and GEMM output-bound receipts
394
444
  - `SignedSession` high-level API
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "rns_engine"
7
- version = "0.7.0"
7
+ version = "0.8.0"
8
8
  description = "Exact RNS engine with session/cache API."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -57,6 +57,17 @@ from .weighted import (
57
57
  accumulate_weighted_int32,
58
58
  certify_weighted_sum_bound,
59
59
  )
60
+ from .capacity import (
61
+ DigitPlaneGemmCapacityPlan,
62
+ SignedCapacityPlan,
63
+ plan_digit_plane_gemm_capacity,
64
+ plan_signed_capacity,
65
+ plan_weighted_sum_capacity,
66
+ )
67
+ from .coefficients import (
68
+ GroupedCoefficientCapacityPlan,
69
+ plan_grouped_coefficient_capacity,
70
+ )
60
71
 
61
72
  try:
62
73
  __version__ = _distribution_version("rns_engine")
@@ -83,6 +94,10 @@ __all__ = [
83
94
  "EncodedArray", "SessionCache", "Session", "SignedSession",
84
95
  "SignedRangeCertificate", "certify_signed_bound", "certify_signed_dot_bound",
85
96
  "WeightedInt32Result", "accumulate_weighted_int32", "certify_weighted_sum_bound",
97
+ "SignedCapacityPlan", "DigitPlaneGemmCapacityPlan",
98
+ "GroupedCoefficientCapacityPlan",
99
+ "plan_signed_capacity", "plan_weighted_sum_capacity",
100
+ "plan_digit_plane_gemm_capacity", "plan_grouped_coefficient_capacity",
86
101
  "info",
87
102
  ]
88
103
 
@@ -96,3 +111,4 @@ def info():
96
111
  print(f" AVX2 : {'yes' if HAS_AVX2 else 'no'}")
97
112
  print(" Core APIs : add/sub/mul/fma + raw/omp/auto scalar-broadcast family")
98
113
  print(" Bridge APIs : weighted signed INT32 accumulation with range receipts")
114
+ print(" Planning APIs : local coefficient safety + global digit-plane RNS capacity")
@@ -0,0 +1,390 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from math import gcd, prod
5
+ from operator import index
6
+ from typing import Any, Iterable, Sequence
7
+
8
+ from ._core import M
9
+
10
+
11
+ def _require_integer(value: Any, *, name: str) -> int:
12
+ try:
13
+ return index(value)
14
+ except TypeError as exc:
15
+ raise TypeError(f"{name} must be an integer") from exc
16
+
17
+
18
+ def _require_nonnegative_integer(value: Any, *, name: str) -> int:
19
+ integer = _require_integer(value, name=name)
20
+ if integer < 0:
21
+ raise ValueError(f"{name} must be >= 0")
22
+ return integer
23
+
24
+
25
+ def _require_radix(value: Any) -> int:
26
+ radix = _require_integer(value, name="radix")
27
+ if radix < 2:
28
+ raise ValueError("radix must be >= 2")
29
+ return radix
30
+
31
+
32
+ def _nonnegative_tuple(values: Iterable[Any], *, name: str) -> tuple[int, ...]:
33
+ try:
34
+ raw = tuple(values)
35
+ except TypeError as exc:
36
+ raise TypeError(f"{name} must be an iterable of nonnegative integers") from exc
37
+
38
+ return tuple(
39
+ _require_nonnegative_integer(value, name=f"{name}[{position}]")
40
+ for position, value in enumerate(raw)
41
+ )
42
+
43
+
44
+ def _integer_tuple(values: Iterable[Any], *, name: str) -> tuple[int, ...]:
45
+ try:
46
+ raw = tuple(values)
47
+ except TypeError as exc:
48
+ raise TypeError(f"{name} must be an iterable of integers") from exc
49
+
50
+ return tuple(
51
+ _require_integer(value, name=f"{name}[{position}]")
52
+ for position, value in enumerate(raw)
53
+ )
54
+
55
+
56
+ def _validate_additional_moduli(values: Iterable[Any]) -> tuple[int, ...]:
57
+ moduli = _integer_tuple(values, name="additional_moduli")
58
+ running_product = int(M)
59
+
60
+ for position, modulus in enumerate(moduli):
61
+ if modulus <= 1:
62
+ raise ValueError(f"additional_moduli[{position}] must be > 1")
63
+
64
+ common_factor = gcd(running_product, modulus)
65
+ if common_factor != 1:
66
+ raise ValueError(
67
+ f"additional_moduli[{position}]={modulus} must be coprime with "
68
+ f"the existing modulus product; gcd={common_factor}"
69
+ )
70
+ running_product *= modulus
71
+
72
+ return moduli
73
+
74
+
75
+ def _ceil_div(numerator: int, denominator: int) -> int:
76
+ return (numerator + denominator - 1) // denominator
77
+
78
+
79
+ @dataclass(frozen=True, slots=True)
80
+ class SignedCapacityPlan:
81
+ """Exact modulus-capacity receipt for a symmetric signed result bound.
82
+
83
+ The current engine uniquely represents every integer in ``[-bound, bound]``
84
+ only when its modulus is at least ``2 * bound + 1``. Optional additional
85
+ moduli are treated as proposed CRT rails and must be pairwise coprime with
86
+ the current four-rail modulus product and with one another.
87
+ """
88
+
89
+ max_abs_bound: int
90
+ additional_moduli: tuple[int, ...] = ()
91
+
92
+ def __post_init__(self) -> None:
93
+ bound = _require_nonnegative_integer(
94
+ self.max_abs_bound,
95
+ name="max_abs_bound",
96
+ )
97
+ moduli = _validate_additional_moduli(self.additional_moduli)
98
+ object.__setattr__(self, "max_abs_bound", bound)
99
+ object.__setattr__(self, "additional_moduli", moduli)
100
+
101
+ @property
102
+ def current_modulus(self) -> int:
103
+ return int(M)
104
+
105
+ @property
106
+ def additional_modulus_product(self) -> int:
107
+ return prod(self.additional_moduli, start=1)
108
+
109
+ @property
110
+ def available_modulus(self) -> int:
111
+ return self.current_modulus * self.additional_modulus_product
112
+
113
+ @property
114
+ def minimum_required_modulus(self) -> int:
115
+ return 2 * self.max_abs_bound + 1
116
+
117
+ @property
118
+ def current_unique(self) -> bool:
119
+ return self.current_modulus >= self.minimum_required_modulus
120
+
121
+ @property
122
+ def unique(self) -> bool:
123
+ return self.available_modulus >= self.minimum_required_modulus
124
+
125
+ @property
126
+ def current_headroom(self) -> int:
127
+ return (self.current_modulus - 1) // 2 - self.max_abs_bound
128
+
129
+ @property
130
+ def available_headroom(self) -> int:
131
+ return (self.available_modulus - 1) // 2 - self.max_abs_bound
132
+
133
+ @property
134
+ def modulus_shortfall(self) -> int:
135
+ return max(0, self.minimum_required_modulus - self.available_modulus)
136
+
137
+ @property
138
+ def minimum_additional_product_factor(self) -> int:
139
+ """Smallest exact product factor needed beyond the current modulus."""
140
+
141
+ return max(
142
+ 1,
143
+ _ceil_div(self.minimum_required_modulus, self.current_modulus),
144
+ )
145
+
146
+ @property
147
+ def minimum_single_coprime_modulus(self) -> int:
148
+ """Smallest one-rail modulus that closes the current capacity deficit.
149
+
150
+ This is a mathematical CRT-capacity result, not a claim that the
151
+ returned modulus has efficient reduction or hardware properties.
152
+ """
153
+
154
+ candidate = self.minimum_additional_product_factor
155
+ if candidate <= 1:
156
+ return 1
157
+ while gcd(candidate, self.current_modulus) != 1:
158
+ candidate += 1
159
+ return candidate
160
+
161
+ @property
162
+ def additional_bits_required(self) -> int:
163
+ """Power-of-two bit budget needed beyond the current modulus product."""
164
+
165
+ factor = self.minimum_additional_product_factor
166
+ return 0 if factor <= 1 else (factor - 1).bit_length()
167
+
168
+ @property
169
+ def required_modulus_bits(self) -> int:
170
+ return self.minimum_required_modulus.bit_length()
171
+
172
+ @property
173
+ def available_modulus_bits(self) -> int:
174
+ return self.available_modulus.bit_length()
175
+
176
+ def require_unique(self) -> SignedCapacityPlan:
177
+ if not self.unique:
178
+ raise OverflowError(
179
+ "signed result is modular-only: available modulus "
180
+ f"{self.available_modulus} is smaller than the required modulus "
181
+ f"{self.minimum_required_modulus} for absolute bound "
182
+ f"{self.max_abs_bound}"
183
+ )
184
+ return self
185
+
186
+
187
+ def plan_signed_capacity(
188
+ max_abs_bound: int,
189
+ additional_moduli: Sequence[int] | Iterable[int] = (),
190
+ ) -> SignedCapacityPlan:
191
+ """Plan exact signed capacity for a known absolute result bound."""
192
+
193
+ return SignedCapacityPlan(
194
+ max_abs_bound=max_abs_bound,
195
+ additional_moduli=tuple(additional_moduli),
196
+ )
197
+
198
+
199
+ def plan_weighted_sum_capacity(
200
+ weights: Sequence[int] | Iterable[int],
201
+ term_abs_bounds: Sequence[int] | Iterable[int],
202
+ addend_abs_bound: int = 0,
203
+ additional_moduli: Sequence[int] | Iterable[int] = (),
204
+ ) -> SignedCapacityPlan:
205
+ """Plan capacity for ``sum(weight[t] * term[t]) + addend`` exactly."""
206
+
207
+ exact_weights = _integer_tuple(weights, name="weights")
208
+ exact_bounds = _nonnegative_tuple(term_abs_bounds, name="term_abs_bounds")
209
+ if len(exact_weights) != len(exact_bounds):
210
+ raise ValueError(
211
+ "weights and term_abs_bounds must have the same length "
212
+ f"({len(exact_weights)} != {len(exact_bounds)})"
213
+ )
214
+
215
+ addend = _require_nonnegative_integer(
216
+ addend_abs_bound,
217
+ name="addend_abs_bound",
218
+ )
219
+ bound = addend + sum(
220
+ abs(weight) * term_bound
221
+ for weight, term_bound in zip(exact_weights, exact_bounds)
222
+ )
223
+ return plan_signed_capacity(bound, additional_moduli)
224
+
225
+
226
+ @dataclass(frozen=True, slots=True)
227
+ class DigitPlaneGemmCapacityPlan:
228
+ """Exact worst-case capacity plan for positional digit-plane GEMM.
229
+
230
+ If ``A = sum(A_i * radix**i)`` and ``B = sum(B_j * radix**j)``, with
231
+ per-plane entry magnitude bounds ``left_digit_abs_bounds[i]`` and
232
+ ``right_digit_abs_bounds[j]``, then one output entry obeys::
233
+
234
+ |C| <= K * left_value_abs_bound * right_value_abs_bound + addend
235
+
236
+ This is algebraically identical to summing every plane-pair GEMM with its
237
+ positional weight ``radix**(i + j)``. No floating point is used.
238
+ """
239
+
240
+ inner_dimension: int
241
+ radix: int
242
+ left_digit_abs_bounds: tuple[int, ...]
243
+ right_digit_abs_bounds: tuple[int, ...]
244
+ addend_abs_bound: int = 0
245
+ additional_moduli: tuple[int, ...] = ()
246
+
247
+ def __post_init__(self) -> None:
248
+ object.__setattr__(
249
+ self,
250
+ "inner_dimension",
251
+ _require_nonnegative_integer(
252
+ self.inner_dimension,
253
+ name="inner_dimension",
254
+ ),
255
+ )
256
+ object.__setattr__(self, "radix", _require_radix(self.radix))
257
+ object.__setattr__(
258
+ self,
259
+ "left_digit_abs_bounds",
260
+ _nonnegative_tuple(
261
+ self.left_digit_abs_bounds,
262
+ name="left_digit_abs_bounds",
263
+ ),
264
+ )
265
+ object.__setattr__(
266
+ self,
267
+ "right_digit_abs_bounds",
268
+ _nonnegative_tuple(
269
+ self.right_digit_abs_bounds,
270
+ name="right_digit_abs_bounds",
271
+ ),
272
+ )
273
+ object.__setattr__(
274
+ self,
275
+ "addend_abs_bound",
276
+ _require_nonnegative_integer(
277
+ self.addend_abs_bound,
278
+ name="addend_abs_bound",
279
+ ),
280
+ )
281
+ object.__setattr__(
282
+ self,
283
+ "additional_moduli",
284
+ _validate_additional_moduli(self.additional_moduli),
285
+ )
286
+
287
+ @property
288
+ def left_plane_count(self) -> int:
289
+ return len(self.left_digit_abs_bounds)
290
+
291
+ @property
292
+ def right_plane_count(self) -> int:
293
+ return len(self.right_digit_abs_bounds)
294
+
295
+ @property
296
+ def plane_pair_count(self) -> int:
297
+ return self.left_plane_count * self.right_plane_count
298
+
299
+ @property
300
+ def grouped_coefficient_count(self) -> int:
301
+ if self.left_plane_count == 0 or self.right_plane_count == 0:
302
+ return 0
303
+ return self.left_plane_count + self.right_plane_count - 1
304
+
305
+ @property
306
+ def left_value_abs_bound(self) -> int:
307
+ return sum(
308
+ digit_bound * self.radix**position
309
+ for position, digit_bound in enumerate(self.left_digit_abs_bounds)
310
+ )
311
+
312
+ @property
313
+ def right_value_abs_bound(self) -> int:
314
+ return sum(
315
+ digit_bound * self.radix**position
316
+ for position, digit_bound in enumerate(self.right_digit_abs_bounds)
317
+ )
318
+
319
+ @property
320
+ def plane_pair_abs_bounds(self) -> tuple[int, ...]:
321
+ return tuple(
322
+ self.inner_dimension * left_bound * right_bound
323
+ for left_bound in self.left_digit_abs_bounds
324
+ for right_bound in self.right_digit_abs_bounds
325
+ )
326
+
327
+ @property
328
+ def plane_pair_weights(self) -> tuple[int, ...]:
329
+ return tuple(
330
+ self.radix ** (left_position + right_position)
331
+ for left_position in range(self.left_plane_count)
332
+ for right_position in range(self.right_plane_count)
333
+ )
334
+
335
+ @property
336
+ def max_abs_bound(self) -> int:
337
+ return (
338
+ self.inner_dimension
339
+ * self.left_value_abs_bound
340
+ * self.right_value_abs_bound
341
+ + self.addend_abs_bound
342
+ )
343
+
344
+ @property
345
+ def capacity(self) -> SignedCapacityPlan:
346
+ return plan_signed_capacity(
347
+ self.max_abs_bound,
348
+ self.additional_moduli,
349
+ )
350
+
351
+ @property
352
+ def current_unique(self) -> bool:
353
+ return self.capacity.current_unique
354
+
355
+ @property
356
+ def unique(self) -> bool:
357
+ return self.capacity.unique
358
+
359
+ def require_unique(self) -> DigitPlaneGemmCapacityPlan:
360
+ self.capacity.require_unique()
361
+ return self
362
+
363
+
364
+ def plan_digit_plane_gemm_capacity(
365
+ inner_dimension: int,
366
+ radix: int,
367
+ left_digit_abs_bounds: Sequence[int] | Iterable[int],
368
+ right_digit_abs_bounds: Sequence[int] | Iterable[int],
369
+ addend_abs_bound: int = 0,
370
+ additional_moduli: Sequence[int] | Iterable[int] = (),
371
+ ) -> DigitPlaneGemmCapacityPlan:
372
+ """Build an exact positional digit-plane GEMM capacity receipt."""
373
+
374
+ return DigitPlaneGemmCapacityPlan(
375
+ inner_dimension=inner_dimension,
376
+ radix=radix,
377
+ left_digit_abs_bounds=tuple(left_digit_abs_bounds),
378
+ right_digit_abs_bounds=tuple(right_digit_abs_bounds),
379
+ addend_abs_bound=addend_abs_bound,
380
+ additional_moduli=tuple(additional_moduli),
381
+ )
382
+
383
+
384
+ __all__ = [
385
+ "DigitPlaneGemmCapacityPlan",
386
+ "SignedCapacityPlan",
387
+ "plan_digit_plane_gemm_capacity",
388
+ "plan_signed_capacity",
389
+ "plan_weighted_sum_capacity",
390
+ ]
@@ -0,0 +1,179 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from operator import index
5
+ from typing import Any, Iterable, Sequence
6
+
7
+
8
+ def _require_integer(value: Any, *, name: str) -> int:
9
+ try:
10
+ return index(value)
11
+ except TypeError as exc:
12
+ raise TypeError(f"{name} must be an integer") from exc
13
+
14
+
15
+ def _require_nonnegative_integer(value: Any, *, name: str) -> int:
16
+ integer = _require_integer(value, name=name)
17
+ if integer < 0:
18
+ raise ValueError(f"{name} must be >= 0")
19
+ return integer
20
+
21
+
22
+ def _nonnegative_tuple(values: Iterable[Any], *, name: str) -> tuple[int, ...]:
23
+ try:
24
+ raw = tuple(values)
25
+ except TypeError as exc:
26
+ raise TypeError(f"{name} must be an iterable of nonnegative integers") from exc
27
+
28
+ return tuple(
29
+ _require_nonnegative_integer(value, name=f"{name}[{position}]")
30
+ for position, value in enumerate(raw)
31
+ )
32
+
33
+
34
+ def _ceil_log2(value: int) -> int:
35
+ if value <= 1:
36
+ return 0
37
+ return (value - 1).bit_length()
38
+
39
+
40
+ @dataclass(frozen=True, slots=True)
41
+ class GroupedCoefficientCapacityPlan:
42
+ """Exact local-accumulator receipt for grouped digit-plane products.
43
+
44
+ For raw positional coefficients
45
+
46
+ ``R[k] = sum_{i+j=k} GEMM(A_i, B_j)``,
47
+
48
+ each contributing plane-pair GEMM has magnitude bound
49
+
50
+ ``inner_dimension * left_bound[i] * right_bound[j]``.
51
+
52
+ This receipt answers whether those grouped coefficients fit a signed native
53
+ accumulator such as INT32. It deliberately does not answer whether the
54
+ later radix-weighted wide reconstruction fits the global RNS modulus; that
55
+ is handled by :class:`DigitPlaneGemmCapacityPlan`.
56
+ """
57
+
58
+ inner_dimension: int
59
+ left_digit_abs_bounds: tuple[int, ...]
60
+ right_digit_abs_bounds: tuple[int, ...]
61
+ accumulator_bits: int = 32
62
+
63
+ def __post_init__(self) -> None:
64
+ object.__setattr__(
65
+ self,
66
+ "inner_dimension",
67
+ _require_nonnegative_integer(
68
+ self.inner_dimension,
69
+ name="inner_dimension",
70
+ ),
71
+ )
72
+ object.__setattr__(
73
+ self,
74
+ "left_digit_abs_bounds",
75
+ _nonnegative_tuple(
76
+ self.left_digit_abs_bounds,
77
+ name="left_digit_abs_bounds",
78
+ ),
79
+ )
80
+ object.__setattr__(
81
+ self,
82
+ "right_digit_abs_bounds",
83
+ _nonnegative_tuple(
84
+ self.right_digit_abs_bounds,
85
+ name="right_digit_abs_bounds",
86
+ ),
87
+ )
88
+ bits = _require_integer(self.accumulator_bits, name="accumulator_bits")
89
+ if bits < 1:
90
+ raise ValueError("accumulator_bits must be >= 1")
91
+ object.__setattr__(self, "accumulator_bits", bits)
92
+
93
+ @property
94
+ def coefficient_count(self) -> int:
95
+ if not self.left_digit_abs_bounds or not self.right_digit_abs_bounds:
96
+ return 0
97
+ return len(self.left_digit_abs_bounds) + len(self.right_digit_abs_bounds) - 1
98
+
99
+ @property
100
+ def plane_pair_count(self) -> int:
101
+ return len(self.left_digit_abs_bounds) * len(self.right_digit_abs_bounds)
102
+
103
+ @property
104
+ def coefficient_pair_counts(self) -> tuple[int, ...]:
105
+ return tuple(
106
+ sum(
107
+ 1
108
+ for left_position in range(len(self.left_digit_abs_bounds))
109
+ for right_position in range(len(self.right_digit_abs_bounds))
110
+ if left_position + right_position == coefficient_position
111
+ )
112
+ for coefficient_position in range(self.coefficient_count)
113
+ )
114
+
115
+ @property
116
+ def coefficient_abs_bounds(self) -> tuple[int, ...]:
117
+ return tuple(
118
+ sum(
119
+ self.inner_dimension
120
+ * self.left_digit_abs_bounds[left_position]
121
+ * self.right_digit_abs_bounds[right_position]
122
+ for left_position in range(len(self.left_digit_abs_bounds))
123
+ for right_position in range(len(self.right_digit_abs_bounds))
124
+ if left_position + right_position == coefficient_position
125
+ )
126
+ for coefficient_position in range(self.coefficient_count)
127
+ )
128
+
129
+ @property
130
+ def max_abs_bound(self) -> int:
131
+ return max(self.coefficient_abs_bounds, default=0)
132
+
133
+ @property
134
+ def signed_accumulator_limit(self) -> int:
135
+ return (1 << (self.accumulator_bits - 1)) - 1
136
+
137
+ @property
138
+ def safe(self) -> bool:
139
+ return self.max_abs_bound <= self.signed_accumulator_limit
140
+
141
+ @property
142
+ def headroom(self) -> int:
143
+ return self.signed_accumulator_limit - self.max_abs_bound
144
+
145
+ @property
146
+ def minimum_signed_accumulator_bits(self) -> int:
147
+ return 1 + _ceil_log2(self.max_abs_bound + 1)
148
+
149
+ def require_safe(self) -> GroupedCoefficientCapacityPlan:
150
+ if not self.safe:
151
+ raise OverflowError(
152
+ "grouped coefficient exceeds the signed accumulator: bound "
153
+ f"{self.max_abs_bound} is greater than the "
154
+ f"{self.accumulator_bits}-bit limit "
155
+ f"{self.signed_accumulator_limit}"
156
+ )
157
+ return self
158
+
159
+
160
+ def plan_grouped_coefficient_capacity(
161
+ inner_dimension: int,
162
+ left_digit_abs_bounds: Sequence[int] | Iterable[int],
163
+ right_digit_abs_bounds: Sequence[int] | Iterable[int],
164
+ accumulator_bits: int = 32,
165
+ ) -> GroupedCoefficientCapacityPlan:
166
+ """Build an exact local grouped-coefficient accumulator receipt."""
167
+
168
+ return GroupedCoefficientCapacityPlan(
169
+ inner_dimension=inner_dimension,
170
+ left_digit_abs_bounds=tuple(left_digit_abs_bounds),
171
+ right_digit_abs_bounds=tuple(right_digit_abs_bounds),
172
+ accumulator_bits=accumulator_bits,
173
+ )
174
+
175
+
176
+ __all__ = [
177
+ "GroupedCoefficientCapacityPlan",
178
+ "plan_grouped_coefficient_capacity",
179
+ ]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rns_engine
3
- Version: 0.7.0
3
+ Version: 0.8.0
4
4
  Summary: Exact RNS engine with session/cache API.
5
5
  Author: Evan Wesley
6
6
  License-Expression: AGPL-3.0-only
@@ -194,7 +194,7 @@ out = session.decode_signed(session.add(a, b))
194
194
 
195
195
  ## Weighted INT32 partial accumulation
196
196
 
197
- Version 0.7 provides the fused direct bridge for digit-plane, chunked-GEMM, and Tensor Core pipelines.
197
+ Version 0.7 introduced the fused direct bridge for digit-plane, chunked-GEMM, and Tensor Core pipelines.
198
198
 
199
199
  Give the engine signed INT32 partial outputs with shape:
200
200
 
@@ -246,7 +246,7 @@ Important details:
246
246
  - `decode_modular()` always returns the canonical residue in `[0, M)`.
247
247
  - Empty term sets produce an exact zero result.
248
248
 
249
- The current implementation uses one fused native `_weighted` call to read signed INT32 partials, collect exact per-term magnitude bounds, apply positional weights modulo `M`, and accumulate all four rails. The pre-fusion staged encode → scale → add body remains available internally as an exact reference for A/B tests. Local AVX2 diagnostics measured roughly 2.1×–2.9× speedup at one million outputs and larger gains on tiny many-term cases; those figures are hardware-specific CPU evidence, not CUDA, Tensor Core, or universal performance claims.
249
+ The implementation uses one fused native `_weighted` call to read signed INT32 partials, collect exact per-term magnitude bounds, apply positional weights modulo `M`, and accumulate all four rails. The pre-fusion staged encode → scale → add body remains available internally as an exact reference for A/B tests. Local AVX2 diagnostics measured roughly 2.1×–2.9× speedup at one million outputs and larger gains on tiny many-term cases; those figures are hardware-specific CPU evidence, not CUDA, Tensor Core, or universal performance claims.
250
250
 
251
251
  A precomputed receipt can also be created without materializing partial arrays:
252
252
 
@@ -260,6 +260,45 @@ certificate.require_unique()
260
260
 
261
261
  ---
262
262
 
263
+ ## Exact GEMM capacity planning
264
+
265
+ Version 0.8 separates two different safety questions that must not be conflated:
266
+
267
+ 1. Does each grouped raw digit-plane coefficient fit the native signed accumulator?
268
+ 2. Does the final radix-weighted result fit uniquely inside the global RNS modulus product?
269
+
270
+ ```python
271
+ import rns_engine as rns
272
+
273
+ local = rns.plan_grouped_coefficient_capacity(
274
+ inner_dimension=5120,
275
+ left_digit_abs_bounds=[127] * 8,
276
+ right_digit_abs_bounds=[127] * 8,
277
+ accumulator_bits=32,
278
+ )
279
+
280
+ global_plan = rns.plan_digit_plane_gemm_capacity(
281
+ inner_dimension=5120,
282
+ radix=128,
283
+ left_digit_abs_bounds=[127] * 8,
284
+ right_digit_abs_bounds=[127] * 8,
285
+ )
286
+
287
+ assert local.max_abs_bound == 660_643_840
288
+ assert local.safe
289
+ assert local.minimum_signed_accumulator_bits == 31
290
+
291
+ assert global_plan.capacity.required_modulus_bits == 126
292
+ assert global_plan.capacity.additional_bits_required == 71
293
+ assert not global_plan.current_unique
294
+ ```
295
+
296
+ That workload is locally safe in INT32 but globally modular-only in the current four-rail range. The planner can also validate explicit proposed CRT moduli and rejects candidates that are not pairwise coprime with the existing product.
297
+
298
+ See [`CAPACITY_PLANNING.md`](CAPACITY_PLANNING.md) for the exact laws, examples, and limits of what the receipts prove.
299
+
300
+ ---
301
+
263
302
  ## Core API
264
303
 
265
304
  ### Rail operations
@@ -285,6 +324,16 @@ certificate.require_unique()
285
324
  - `WeightedInt32Result.decode_signed(require_unique=True)`
286
325
  - `WeightedInt32Result.decode_modular()`
287
326
 
327
+ ### Capacity planning
328
+
329
+ - `plan_signed_capacity(max_abs_bound, additional_moduli=())`
330
+ - `plan_weighted_sum_capacity(weights, term_abs_bounds, addend_abs_bound=0, additional_moduli=())`
331
+ - `plan_digit_plane_gemm_capacity(inner_dimension, radix, left_digit_abs_bounds, right_digit_abs_bounds, ...)`
332
+ - `plan_grouped_coefficient_capacity(inner_dimension, left_digit_abs_bounds, right_digit_abs_bounds, accumulator_bits=32)`
333
+ - `SignedCapacityPlan`
334
+ - `DigitPlaneGemmCapacityPlan`
335
+ - `GroupedCoefficientCapacityPlan`
336
+
288
337
  ### Scalar-broadcast encoded operations
289
338
 
290
339
  - `mul_u64(...)`
@@ -359,7 +408,7 @@ Workload: `affine_repeat_u64_io(x, 1_000_003, 7, iterations=1000)`
359
408
  - OpenMP with one thread: **82.80 billion ops/sec**
360
409
  - OpenMP with two threads: **94.86 billion ops/sec**
361
410
 
362
- These measurements cover the existing affine kernels, not the new weighted INT32 orchestration path.
411
+ These measurements cover the existing affine kernels, not weighted INT32 reconstruction or the pure-Python capacity planners.
363
412
 
364
413
  ---
365
414
 
@@ -404,14 +453,15 @@ rns.HAS_AVX2
404
453
 
405
454
  ## Current release
406
455
 
407
- ### v0.7.0
456
+ ### v0.8.0
408
457
 
458
+ - exact local grouped-coefficient accumulator planning
459
+ - exact global digit-plane GEMM RNS-capacity planning
460
+ - strict separation between local INT32 safety and global signed uniqueness
461
+ - candidate CRT-modulus validation with pairwise-coprime enforcement
462
+ - exact modulus shortfall and additional-bit receipts
409
463
  - fused native weighted signed INT32 partial accumulation
410
- - one compiled pass for magnitude collection, positional weighting, and four-rail accumulation
411
- - staged reference body retained for exact fused-versus-staged A/B benchmarking
412
- - exact arbitrary-precision positional weights
413
- - per-term magnitude receipts and strict no-wrap certification
414
- - canonical modular and uniqueness-gated signed decode paths
464
+ - arbitrary-precision positional weights and no-wrap certificates
415
465
  - canonical centered signed encoding and decoding
416
466
  - exact dot-product and GEMM output-bound receipts
417
467
  - `SignedSession` high-level API
@@ -5,6 +5,8 @@ setup.py
5
5
  src/rns_engine/__init__.py
6
6
  src/rns_engine/_core.cpp
7
7
  src/rns_engine/_weighted.cpp
8
+ src/rns_engine/capacity.py
9
+ src/rns_engine/coefficients.py
8
10
  src/rns_engine/engine.py
9
11
  src/rns_engine/signed.py
10
12
  src/rns_engine/weighted.py
@@ -14,6 +16,8 @@ src/rns_engine.egg-info/dependency_links.txt
14
16
  src/rns_engine.egg-info/not-zip-safe
15
17
  src/rns_engine.egg-info/requires.txt
16
18
  src/rns_engine.egg-info/top_level.txt
19
+ tests/test_capacity.py
20
+ tests/test_coefficients.py
17
21
  tests/test_packaging.py
18
22
  tests/test_rns.py
19
23
  tests/test_session.py
@@ -0,0 +1,161 @@
1
+ import math
2
+
3
+ import pytest
4
+
5
+ import rns_engine as rns
6
+
7
+
8
+ def test_single_plane_int8_gemm_fits_current_four_rail_signed_range():
9
+ plan = rns.plan_digit_plane_gemm_capacity(
10
+ inner_dimension=5120,
11
+ radix=128,
12
+ left_digit_abs_bounds=[127],
13
+ right_digit_abs_bounds=[127],
14
+ )
15
+
16
+ assert plan.plane_pair_count == 1
17
+ assert plan.grouped_coefficient_count == 1
18
+ assert plan.left_value_abs_bound == 127
19
+ assert plan.right_value_abs_bound == 127
20
+ assert plan.max_abs_bound == 82_580_480
21
+ assert plan.current_unique is True
22
+ assert plan.unique is True
23
+ assert plan.capacity.additional_bits_required == 0
24
+ assert plan.require_unique() is plan
25
+
26
+
27
+ def test_radix128_plane_count_crosses_current_capacity_at_three_planes():
28
+ two_planes = rns.plan_digit_plane_gemm_capacity(
29
+ 5120,
30
+ 128,
31
+ [127, 127],
32
+ [127, 127],
33
+ )
34
+ three_planes = rns.plan_digit_plane_gemm_capacity(
35
+ 5120,
36
+ 128,
37
+ [127, 127, 127],
38
+ [127, 127, 127],
39
+ )
40
+
41
+ assert two_planes.left_value_abs_bound == 128**2 - 1
42
+ assert two_planes.current_unique is True
43
+ assert three_planes.left_value_abs_bound == 128**3 - 1
44
+ assert three_planes.current_unique is False
45
+ with pytest.raises(OverflowError, match="modular-only"):
46
+ three_planes.require_unique()
47
+
48
+
49
+ def test_eight_full_radix128_planes_need_126_bit_modulus():
50
+ plan = rns.plan_digit_plane_gemm_capacity(
51
+ inner_dimension=5120,
52
+ radix=128,
53
+ left_digit_abs_bounds=[127] * 8,
54
+ right_digit_abs_bounds=[127] * 8,
55
+ )
56
+
57
+ full_value_bound = 128**8 - 1
58
+ expected_output_bound = 5120 * full_value_bound**2
59
+
60
+ assert plan.left_value_abs_bound == full_value_bound
61
+ assert plan.right_value_abs_bound == full_value_bound
62
+ assert plan.plane_pair_count == 64
63
+ assert plan.grouped_coefficient_count == 15
64
+ assert plan.max_abs_bound == expected_output_bound
65
+ assert plan.capacity.minimum_required_modulus == 2 * expected_output_bound + 1
66
+ assert plan.capacity.required_modulus_bits == 126
67
+ assert plan.capacity.additional_bits_required == 71
68
+ assert plan.current_unique is False
69
+
70
+
71
+ def test_plane_pair_ledger_matches_factored_value_bound_exactly():
72
+ plan = rns.plan_digit_plane_gemm_capacity(
73
+ inner_dimension=37,
74
+ radix=16,
75
+ left_digit_abs_bounds=[7, 3, 1],
76
+ right_digit_abs_bounds=[5, 2],
77
+ addend_abs_bound=11,
78
+ )
79
+
80
+ expanded = rns.plan_weighted_sum_capacity(
81
+ plan.plane_pair_weights,
82
+ plan.plane_pair_abs_bounds,
83
+ addend_abs_bound=11,
84
+ )
85
+
86
+ assert expanded.max_abs_bound == plan.max_abs_bound
87
+ assert plan.plane_pair_weights == (1, 16, 16, 256, 256, 4096)
88
+ assert plan.plane_pair_abs_bounds == (
89
+ 37 * 7 * 5,
90
+ 37 * 7 * 2,
91
+ 37 * 3 * 5,
92
+ 37 * 3 * 2,
93
+ 37 * 1 * 5,
94
+ 37 * 1 * 2,
95
+ )
96
+
97
+
98
+ def test_candidate_extra_rail_is_checked_by_exact_coprime_product():
99
+ baseline = rns.plan_digit_plane_gemm_capacity(
100
+ 5120,
101
+ 128,
102
+ [127] * 8,
103
+ [127] * 8,
104
+ )
105
+ extra_modulus = baseline.capacity.minimum_single_coprime_modulus
106
+
107
+ expanded = rns.plan_digit_plane_gemm_capacity(
108
+ 5120,
109
+ 128,
110
+ [127] * 8,
111
+ [127] * 8,
112
+ additional_moduli=[extra_modulus],
113
+ )
114
+
115
+ assert math.gcd(extra_modulus, rns.M) == 1
116
+ assert expanded.capacity.available_modulus >= expanded.capacity.minimum_required_modulus
117
+ assert expanded.unique is True
118
+ assert expanded.require_unique() is expanded
119
+
120
+
121
+ def test_capacity_planner_rejects_non_coprime_or_invalid_candidate_rails():
122
+ with pytest.raises(ValueError, match="coprime"):
123
+ rns.plan_signed_capacity(1, additional_moduli=[127])
124
+
125
+ with pytest.raises(ValueError, match="coprime"):
126
+ rns.plan_signed_capacity(1, additional_moduli=[3, 3])
127
+
128
+ with pytest.raises(ValueError, match="> 1"):
129
+ rns.plan_signed_capacity(1, additional_moduli=[1])
130
+
131
+
132
+ def test_capacity_planner_uses_exact_integer_inputs_only():
133
+ with pytest.raises(TypeError, match="max_abs_bound"):
134
+ rns.plan_signed_capacity(1.5)
135
+
136
+ with pytest.raises(ValueError, match="max_abs_bound"):
137
+ rns.plan_signed_capacity(-1)
138
+
139
+ with pytest.raises(ValueError, match="radix"):
140
+ rns.plan_digit_plane_gemm_capacity(1, 1, [1], [1])
141
+
142
+ with pytest.raises(ValueError, match=r"left_digit_abs_bounds\[0\]"):
143
+ rns.plan_digit_plane_gemm_capacity(1, 2, [-1], [1])
144
+
145
+ with pytest.raises(ValueError, match="same length"):
146
+ rns.plan_weighted_sum_capacity([1, 2], [3])
147
+
148
+
149
+ def test_empty_digit_plane_body_reduces_to_addend_capacity():
150
+ plan = rns.plan_digit_plane_gemm_capacity(
151
+ inner_dimension=5120,
152
+ radix=128,
153
+ left_digit_abs_bounds=[],
154
+ right_digit_abs_bounds=[127],
155
+ addend_abs_bound=9,
156
+ )
157
+
158
+ assert plan.plane_pair_count == 0
159
+ assert plan.grouped_coefficient_count == 0
160
+ assert plan.max_abs_bound == 9
161
+ assert plan.current_unique is True
@@ -0,0 +1,110 @@
1
+ import pytest
2
+
3
+ import rns_engine as rns
4
+
5
+
6
+ def test_single_int8_plane_has_expected_int32_bound():
7
+ plan = rns.plan_grouped_coefficient_capacity(
8
+ inner_dimension=5120,
9
+ left_digit_abs_bounds=[127],
10
+ right_digit_abs_bounds=[127],
11
+ )
12
+
13
+ assert plan.coefficient_count == 1
14
+ assert plan.plane_pair_count == 1
15
+ assert plan.coefficient_pair_counts == (1,)
16
+ assert plan.coefficient_abs_bounds == (82_580_480,)
17
+ assert plan.max_abs_bound == 82_580_480
18
+ assert plan.safe is True
19
+ assert plan.minimum_signed_accumulator_bits == 28
20
+ assert plan.require_safe() is plan
21
+
22
+
23
+ def test_eight_plane_grouped_coefficients_fit_int32_while_global_result_does_not():
24
+ local = rns.plan_grouped_coefficient_capacity(
25
+ inner_dimension=5120,
26
+ left_digit_abs_bounds=[127] * 8,
27
+ right_digit_abs_bounds=[127] * 8,
28
+ accumulator_bits=32,
29
+ )
30
+ global_plan = rns.plan_digit_plane_gemm_capacity(
31
+ inner_dimension=5120,
32
+ radix=128,
33
+ left_digit_abs_bounds=[127] * 8,
34
+ right_digit_abs_bounds=[127] * 8,
35
+ )
36
+
37
+ expected_pair_counts = (1, 2, 3, 4, 5, 6, 7, 8, 7, 6, 5, 4, 3, 2, 1)
38
+ one_pair_bound = 82_580_480
39
+
40
+ assert local.coefficient_pair_counts == expected_pair_counts
41
+ assert local.coefficient_abs_bounds == tuple(
42
+ count * one_pair_bound for count in expected_pair_counts
43
+ )
44
+ assert local.max_abs_bound == 660_643_840
45
+ assert local.minimum_signed_accumulator_bits == 31
46
+ assert local.headroom == 1_486_839_807
47
+ assert local.safe is True
48
+ assert global_plan.current_unique is False
49
+ assert global_plan.capacity.required_modulus_bits == 126
50
+
51
+
52
+ def test_grouped_coefficient_plan_detects_int32_overflow():
53
+ plan = rns.plan_grouped_coefficient_capacity(
54
+ inner_dimension=20_000,
55
+ left_digit_abs_bounds=[127] * 8,
56
+ right_digit_abs_bounds=[127] * 8,
57
+ accumulator_bits=32,
58
+ )
59
+
60
+ assert plan.max_abs_bound == 20_000 * 8 * 127 * 127
61
+ assert plan.safe is False
62
+ assert plan.headroom < 0
63
+ assert plan.minimum_signed_accumulator_bits == 33
64
+ with pytest.raises(OverflowError, match="exceeds"):
65
+ plan.require_safe()
66
+
67
+
68
+ def test_grouped_bounds_match_manual_nonuniform_convolution():
69
+ plan = rns.plan_grouped_coefficient_capacity(
70
+ inner_dimension=11,
71
+ left_digit_abs_bounds=[7, 3, 1],
72
+ right_digit_abs_bounds=[5, 2],
73
+ )
74
+
75
+ assert plan.coefficient_pair_counts == (1, 2, 2, 1)
76
+ assert plan.coefficient_abs_bounds == (
77
+ 11 * (7 * 5),
78
+ 11 * (7 * 2 + 3 * 5),
79
+ 11 * (3 * 2 + 1 * 5),
80
+ 11 * (1 * 2),
81
+ )
82
+
83
+
84
+ def test_empty_plane_body_has_zero_local_bound():
85
+ plan = rns.plan_grouped_coefficient_capacity(
86
+ inner_dimension=5120,
87
+ left_digit_abs_bounds=[],
88
+ right_digit_abs_bounds=[127],
89
+ )
90
+
91
+ assert plan.coefficient_count == 0
92
+ assert plan.coefficient_pair_counts == ()
93
+ assert plan.coefficient_abs_bounds == ()
94
+ assert plan.max_abs_bound == 0
95
+ assert plan.minimum_signed_accumulator_bits == 1
96
+ assert plan.safe is True
97
+
98
+
99
+ def test_grouped_coefficient_inputs_are_strict_integers():
100
+ with pytest.raises(TypeError, match="inner_dimension"):
101
+ rns.plan_grouped_coefficient_capacity(1.5, [1], [1])
102
+
103
+ with pytest.raises(ValueError, match="inner_dimension"):
104
+ rns.plan_grouped_coefficient_capacity(-1, [1], [1])
105
+
106
+ with pytest.raises(ValueError, match=r"left_digit_abs_bounds\[0\]"):
107
+ rns.plan_grouped_coefficient_capacity(1, [-1], [1])
108
+
109
+ with pytest.raises(ValueError, match="accumulator_bits"):
110
+ rns.plan_grouped_coefficient_capacity(1, [1], [1], accumulator_bits=0)
File without changes
File without changes
File without changes
File without changes