rivin-perm 1.0.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 (114) hide show
  1. rivin_perm-1.0.0/BINARY_APPLICATIONS.md +327 -0
  2. rivin_perm-1.0.0/CHANGELOG.md +143 -0
  3. rivin_perm-1.0.0/CONTRIBUTING.md +140 -0
  4. rivin_perm-1.0.0/CUDA.md +123 -0
  5. rivin_perm-1.0.0/LICENSE +21 -0
  6. rivin_perm-1.0.0/LOW_RANK_DISPATCH_RESULTS.md +167 -0
  7. rivin_perm-1.0.0/LOW_RANK_SHOWCASES.md +235 -0
  8. rivin_perm-1.0.0/MANIFEST.in +140 -0
  9. rivin_perm-1.0.0/PACKAGING.md +74 -0
  10. rivin_perm-1.0.0/PKG-INFO +697 -0
  11. rivin_perm-1.0.0/README.md +667 -0
  12. rivin_perm-1.0.0/REPOSITORY_LAYOUT.md +22 -0
  13. rivin_perm-1.0.0/RESEARCH_CORRECTIONS.md +24 -0
  14. rivin_perm-1.0.0/benchmark.py +186 -0
  15. rivin_perm-1.0.0/benchmark_binary.py +134 -0
  16. rivin_perm-1.0.0/benchmark_low_rank.py +188 -0
  17. rivin_perm-1.0.0/benchmark_native_dispatch.py +258 -0
  18. rivin_perm-1.0.0/benchmark_weighted_rook.py +371 -0
  19. rivin_perm-1.0.0/cycle_geodesic.py +262 -0
  20. rivin_perm-1.0.0/experiment_random_cubic.py +443 -0
  21. rivin_perm-1.0.0/notes/benchmarks/README.md +188 -0
  22. rivin_perm-1.0.0/notes/benchmarks/SCHUR.md +210 -0
  23. rivin_perm-1.0.0/notes/benchmarks/WEIGHTED_FRONTIER_NATIVE.md +127 -0
  24. rivin_perm-1.0.0/notes/benchmarks/WEIGHTED_MATCHINGS.md +147 -0
  25. rivin_perm-1.0.0/notes/benchmarks/WEIGHTED_ROOK_DISPATCH.md +64 -0
  26. rivin_perm-1.0.0/notes/benchmarks/check_schur_orbits.py +237 -0
  27. rivin_perm-1.0.0/notes/benchmarks/polynomial_baselines.cpp +138 -0
  28. rivin_perm-1.0.0/notes/benchmarks/polynomial_compare.py +906 -0
  29. rivin_perm-1.0.0/notes/benchmarks/schur_compare.py +722 -0
  30. rivin_perm-1.0.0/notes/benchmarks/schur_compare_wrapper.cu +220 -0
  31. rivin_perm-1.0.0/notes/benchmarks/summarize_polynomial_comparison.py +116 -0
  32. rivin_perm-1.0.0/notes/benchmarks/summarize_weighted_matching_study.py +431 -0
  33. rivin_perm-1.0.0/notes/benchmarks/weighted_matching_frontier.py +231 -0
  34. rivin_perm-1.0.0/notes/benchmarks/weighted_matching_frontier_native.cpp +344 -0
  35. rivin_perm-1.0.0/notes/benchmarks/weighted_matching_frontier_native.py +392 -0
  36. rivin_perm-1.0.0/notes/benchmarks/weighted_matching_partition.cpp +91 -0
  37. rivin_perm-1.0.0/notes/benchmarks/weighted_matching_partition.py +181 -0
  38. rivin_perm-1.0.0/notes/benchmarks/weighted_matching_statistics.py +325 -0
  39. rivin_perm-1.0.0/notes/benchmarks/weighted_matching_study.py +613 -0
  40. rivin_perm-1.0.0/notes/benchmarks/weighted_rook_dispatch.py +200 -0
  41. rivin_perm-1.0.0/notes/frontier/verify_binary_grid_4x4x5_transfer.py +126 -0
  42. rivin_perm-1.0.0/perm.py +1259 -0
  43. rivin_perm-1.0.0/perm_binary.py +1017 -0
  44. rivin_perm-1.0.0/perm_binary_applications.py +552 -0
  45. rivin_perm-1.0.0/perm_binary_cuda.cu +482 -0
  46. rivin_perm-1.0.0/perm_binary_cuda.py +214 -0
  47. rivin_perm-1.0.0/perm_binary_omp.c +550 -0
  48. rivin_perm-1.0.0/perm_c_omp.c +1136 -0
  49. rivin_perm-1.0.0/perm_cuda.py +556 -0
  50. rivin_perm-1.0.0/perm_enum_cuda.cu +482 -0
  51. rivin_perm-1.0.0/perm_float.py +370 -0
  52. rivin_perm-1.0.0/perm_mod3.py +471 -0
  53. rivin_perm-1.0.0/perm_mod3_cuda.cu +385 -0
  54. rivin_perm-1.0.0/perm_mod3_omp.c +327 -0
  55. rivin_perm-1.0.0/perm_modp_cuda.cu +1413 -0
  56. rivin_perm-1.0.0/perm_numba.py +253 -0
  57. rivin_perm-1.0.0/perm_parallel.py +164 -0
  58. rivin_perm-1.0.0/perm_parallel_cuda.cu +619 -0
  59. rivin_perm-1.0.0/perm_polynomial.py +1719 -0
  60. rivin_perm-1.0.0/perm_polynomial_omp.c +1854 -0
  61. rivin_perm-1.0.0/perm_proj_cuda.cu +499 -0
  62. rivin_perm-1.0.0/pyproject.toml +65 -0
  63. rivin_perm-1.0.0/rivin_perm.egg-info/PKG-INFO +697 -0
  64. rivin_perm-1.0.0/rivin_perm.egg-info/SOURCES.txt +112 -0
  65. rivin_perm-1.0.0/rivin_perm.egg-info/dependency_links.txt +1 -0
  66. rivin_perm-1.0.0/rivin_perm.egg-info/requires.txt +10 -0
  67. rivin_perm-1.0.0/rivin_perm.egg-info/top_level.txt +7 -0
  68. rivin_perm-1.0.0/schur_permanents.txt +49 -0
  69. rivin_perm-1.0.0/setup.cfg +4 -0
  70. rivin_perm-1.0.0/setup.py +127 -0
  71. rivin_perm-1.0.0/showcase_low_rank.py +298 -0
  72. rivin_perm-1.0.0/statistics_helpers.py +130 -0
  73. rivin_perm-1.0.0/tests/README.md +16 -0
  74. rivin_perm-1.0.0/tests/test_binary_applications.py +301 -0
  75. rivin_perm-1.0.0/tests/test_binary_cuda_wrappers.py +163 -0
  76. rivin_perm-1.0.0/tests/test_binary_frontier.py +227 -0
  77. rivin_perm-1.0.0/tests/test_binary_native.py +167 -0
  78. rivin_perm-1.0.0/tests/test_binary_permanent.py +504 -0
  79. rivin_perm-1.0.0/tests/test_core.py +238 -0
  80. rivin_perm-1.0.0/tests/test_cuda_crt_planning.py +220 -0
  81. rivin_perm-1.0.0/tests/test_cuda_device_binary.py +175 -0
  82. rivin_perm-1.0.0/tests/test_cuda_device_modular.py +156 -0
  83. rivin_perm-1.0.0/tests/test_cuda_device_parallel.py +100 -0
  84. rivin_perm-1.0.0/tests/test_cuda_device_schur_glynn.py +169 -0
  85. rivin_perm-1.0.0/tests/test_cuda_device_utilities.py +284 -0
  86. rivin_perm-1.0.0/tests/test_cuda_wrappers.py +136 -0
  87. rivin_perm-1.0.0/tests/test_cycle_geodesic.py +175 -0
  88. rivin_perm-1.0.0/tests/test_dispatch_efficiency.py +238 -0
  89. rivin_perm-1.0.0/tests/test_generic_native.py +211 -0
  90. rivin_perm-1.0.0/tests/test_low_rank_dispatch.py +158 -0
  91. rivin_perm-1.0.0/tests/test_low_rank_showcases.py +145 -0
  92. rivin_perm-1.0.0/tests/test_perm_parallel_api.py +48 -0
  93. rivin_perm-1.0.0/tests/test_polynomial_benchmark.py +452 -0
  94. rivin_perm-1.0.0/tests/test_polynomial_native.py +693 -0
  95. rivin_perm-1.0.0/tests/test_polynomial_permanent.py +1783 -0
  96. rivin_perm-1.0.0/tests/test_random_cubic_experiment.py +59 -0
  97. rivin_perm-1.0.0/tests/test_reference_values.py +66 -0
  98. rivin_perm-1.0.0/tests/test_research_statistics.py +229 -0
  99. rivin_perm-1.0.0/tests/test_schur_benchmark.py +260 -0
  100. rivin_perm-1.0.0/tests/test_schur_cpu_glynn.py +518 -0
  101. rivin_perm-1.0.0/tests/test_source_distribution.py +185 -0
  102. rivin_perm-1.0.0/tests/test_weighted_matching_frontier.py +180 -0
  103. rivin_perm-1.0.0/tests/test_weighted_matching_frontier_native.py +531 -0
  104. rivin_perm-1.0.0/tests/test_weighted_matching_partition.py +170 -0
  105. rivin_perm-1.0.0/tests/test_weighted_matching_statistics.py +252 -0
  106. rivin_perm-1.0.0/tests/test_weighted_matching_study.py +334 -0
  107. rivin_perm-1.0.0/tests/test_weighted_matching_summary.py +220 -0
  108. rivin_perm-1.0.0/tests/test_weighted_rook_dispatch.py +337 -0
  109. rivin_perm-1.0.0/tests/test_weighted_rook_planner.py +209 -0
  110. rivin_perm-1.0.0/tests/test_weighted_rook_source_dispatch.py +405 -0
  111. rivin_perm-1.0.0/tools/check_distribution.py +70 -0
  112. rivin_perm-1.0.0/tools/check_installed.py +95 -0
  113. rivin_perm-1.0.0/tools/extract_sdist.py +32 -0
  114. rivin_perm-1.0.0/tools/prepare_release.py +99 -0
@@ -0,0 +1,327 @@
1
+ # Binary permanents: algorithms and application cases
2
+
3
+ The permanent of a zero-one biadjacency matrix is the number of perfect
4
+ matchings of the corresponding bipartite graph. This repository therefore
5
+ keeps binary matrices as a separate exact problem class rather than sending
6
+ them through the general integer kernel.
7
+
8
+ ```python
9
+ from perm_binary import binary_permanent
10
+
11
+ binary_permanent([[1, 1], [1, 1]]) # 2
12
+ ```
13
+
14
+ The default input contract is strict: entries must be integral zeroes or ones.
15
+ `strict=False` explicitly replaces an integral matrix by its support matrix.
16
+ Rectangular inputs are rejected because this API counts perfect matchings of
17
+ both complete bipartition classes.
18
+
19
+ ## Specialized exact algorithm
20
+
21
+ Before exponential work, the implementation:
22
+
23
+ 1. finds a perfect matching, returning zero immediately if Hall feasibility
24
+ fails;
25
+ 2. uses alternating-cycle strongly connected components relative to that
26
+ matching to factor the matrix into independent square blocks; and
27
+ 3. bounds each block with the minimum of `n!`, row/column degree products, and
28
+ rigorously rounded grouped Bregman--Minc bounds.
29
+
30
+ The native residue kernel uses fixed-sign Glynn, so it visits `2^(n-1)` sign
31
+ states. It fixes the densest column, assigns low-degree columns to frequently
32
+ toggled Gray bits, and chooses the cheaper orientation between the matrix and
33
+ its transpose. Signed row sums are small. A Gray transition touches only
34
+ the set bits of one packed column; a precomputed ratio updates the product of
35
+ nonzero row sums while a separate counter handles zero factors.
36
+
37
+ The fast path uses 31-bit Montgomery arithmetic without division in the hot
38
+ loop. A 64-bit Montgomery fallback preserves modular correctness for every
39
+ supported odd signed-64-bit prime greater than the matrix order. Because a
40
+ binary permanent is nonnegative, exact CRT reconstruction needs a modulus
41
+ product greater than the upper bound, not twice that bound.
42
+
43
+ The portable fallback is a definition-level subset dynamic program with
44
+ arbitrary-size Python integers. It is intended for correctness, small
45
+ problems, and installations without OpenMP; dense large blocks should use the
46
+ native backend.
47
+
48
+ ## Live-column frontier algorithm
49
+
50
+ `binary_permanent_frontier` is an exact alternative for sparse matrices whose
51
+ support admits a favorable row order. After each row, a state records which
52
+ currently live columns have already received their matching edge. Processing
53
+ a row selects one adjacent unused column. When the last row adjacent to a
54
+ column has been processed, that column must already be selected; the bit can
55
+ then be forgotten. Different partial matchings with the same live state are
56
+ combined using arbitrary-size Python integers.
57
+
58
+ ```python
59
+ from perm_binary import (
60
+ FrontierStateLimitExceeded,
61
+ binary_permanent_frontier,
62
+ )
63
+
64
+ K4 = [[0, 1, 1, 1],
65
+ [1, 0, 1, 1],
66
+ [1, 1, 0, 1],
67
+ [1, 1, 1, 0]]
68
+
69
+ binary_permanent_frontier(K4) # 9
70
+ ```
71
+
72
+ The full signature is
73
+
74
+ ```python
75
+ binary_permanent_frontier(
76
+ matrix, *, row_order=None, max_states=500_000, strict=True
77
+ )
78
+ ```
79
+
80
+ `row_order=None` invokes a deterministic greedy heuristic. It tries to keep
81
+ the live-column frontier small, but it neither computes nor certifies an
82
+ optimal path decomposition. A supplied order must contain every original
83
+ row index exactly once; it is interpreted before any internal reordering.
84
+ Runtime depends on the distinct reachable states, not on frontier width
85
+ alone, so even a plausible width estimate is not a memory guarantee.
86
+
87
+ `max_states` must be a positive integer. It bounds the number of distinct
88
+ states retained in any one layer. If the next layer would exceed it, an
89
+ explicit call raises `FrontierStateLimitExceeded`; it never changes algorithms
90
+ behind the caller's back. The exception exposes the configured limit, the
91
+ minimum observed state count, and the number of processed rows.
92
+
93
+ The high-level `binary_permanent` dispatcher has deliberately different
94
+ semantics. Only `backend="auto"` may try the frontier route on an eligible
95
+ matching-factor block. If that attempt reaches its internal state cap, the
96
+ dispatcher discards it and continues with the exact native backend when
97
+ available, or the existing portable subset DP otherwise. Explicit
98
+ `backend="native"` and `backend="python"` preserve their selected algorithms.
99
+
100
+ ### Cubic adjacency matrices and cycle covers
101
+
102
+ For the adjacency matrix `A` of an undirected graph, `per(A)` counts directed
103
+ permutation cycle covers: every vertex has one selected outgoing and one
104
+ selected incoming arc. In particular, a transposition selects both
105
+ orientations of one undirected edge and is a permitted directed 2-cycle. This
106
+ is not the same statistic as an oriented spanning 2-factor that requires
107
+ cycles of length at least three.
108
+
109
+ Sparse cubic adjacency matrices are therefore natural frontier candidates,
110
+ but cubicity by itself does not promise that the greedy order will be good.
111
+ The asymmetric 42-vertex evaluation instance has exact permanent `1,361,137`.
112
+ Its independently recorded row order has at most 14 live columns and 2,944
113
+ reachable states, allowing an exact integer frontier computation in a tiny
114
+ fraction of the time required by generic sign-vector enumeration. Other
115
+ cubic instances can have much broader frontiers; the state cap makes that
116
+ instance dependence explicit. Dense matrices, and sparse matrices with poor
117
+ orders, remain better candidates for the native fixed-sign Glynn backend.
118
+
119
+ ## Application 1: bipartite dimers
120
+
121
+ `grid_biadjacency` and `grid_dimer_count` construct parity-bipartitioned
122
+ Cartesian grids with open boundaries and selected periodic axes.
123
+
124
+ ```python
125
+ from perm_binary_applications import grid_dimer_count
126
+
127
+ grid_dimer_count((4, 4)) # 36
128
+ grid_dimer_count((2, 2, 4)) # 121
129
+ grid_dimer_count((2, 3, 4)) # 1845
130
+ grid_dimer_count((4, 4), periodic_axes=(0, 1)) # 272
131
+ ```
132
+
133
+ Open two-dimensional grids are planar Kasteleyn oracles, not a claim that the
134
+ generic permanent is the best planar-dimer algorithm. Doubly periodic grids
135
+ live on a torus and admit fixed-genus Pfaffian treatments. The proposed
136
+ `4 x 4 x 5` grid with periodic x/y axes and open z boundaries is instead a
137
+ genuinely nonplanar 80-vertex benchmark. Its biadjacency matrix has order 40,
138
+ 224 ones, and row/column degree multiset `5^16 6^24`.
139
+
140
+ ```python
141
+ from perm_binary_applications import grid_biadjacency
142
+
143
+ matrix = grid_biadjacency((4, 4, 5), periodic_axes=(0, 1))
144
+ ```
145
+
146
+ The certified bound is
147
+
148
+ `1,393,140,695,040,000,000`,
149
+
150
+ so two descending 31-bit CRT primes suffice. The GH200 host CPU, using 64
151
+ OpenMP threads, computed
152
+
153
+ `per(matrix) = 2,551,276,235,535,120`.
154
+
155
+ The two required residues were:
156
+
157
+ | prime | residue | native kernel wall time |
158
+ |---:|---:|---:|
159
+ | 2,147,483,647 | 1,238,389,710 | 121.97 s |
160
+ | 2,147,483,629 | 1,259,774,250 | 120.42 s |
161
+
162
+ Their 242.38-second sum (4.04 minutes) excludes the independent unused-prime
163
+ holdout. This is an exact finite-volume benchmark, not a novelty claim:
164
+ transfer-matrix and other specialized dimer methods also exist.
165
+
166
+ The optional source-only CUDA path computed the same two residues in 10.25
167
+ and 10.13 seconds, and the complete Python normalization, matching,
168
+ two-residue CRT, and validation path took 22.73 seconds. That is a 10.66x
169
+ end-to-end speedup over the specialized 64-thread CPU call on this host. The
170
+ sm_90 kernel uses 56 registers per thread, 1 KiB of shared memory per block,
171
+ and no stack or spills; a representative order-24 Compute Sanitizer run
172
+ reported zero errors. It is built and imported separately through
173
+ `perm_binary_cuda.py` and does not add a CUDA dependency to the package.
174
+
175
+ As an algorithmically independent check, the standalone layer-transfer
176
+ verifier in `notes/frontier/verify_binary_grid_4x4x5_transfer.py` returned the
177
+ same integer in 2.54 seconds. It is deliberately separate from the permanent
178
+ implementation and demonstrates why highly structured lattices should also
179
+ be compared with problem-specific transfer methods.
180
+
181
+ ### Anisotropic polynomial
182
+
183
+ `weighted_grid_biadjacency` assigns an integer weight by edge axis, while
184
+ `axis_dimer_profile` computes small exact coefficient oracles.
185
+ `axis_dimer_profile_mod` uses the coefficient-valued Glynn backend to compute
186
+ the complete profile modulo any odd signed-64-bit integer, avoiding separate
187
+ evaluation and interpolation calls. The corresponding constant and marked
188
+ edge matrices may instead be passed to `perm_polynomial.affine_permanent` for
189
+ certified exact integer coefficients via CRT. For the open
190
+ `2 x 2 x 2` cube,
191
+
192
+ `Z(w) = 4 + 4 w^2 + w^4`.
193
+
194
+ For the `4 x 4 x 5` periodic-x/y target, the interpolation proposal in the
195
+ source conversation can be sharpened:
196
+
197
+ - each of the 16 length-five z-columns contains at most two vertical dimers,
198
+ so `deg Z <= 32`;
199
+ - every layer has 16 vertices, forcing the number of inter-layer dimers—and
200
+ hence the total vertical count—to be even;
201
+ - therefore `Z(w) = Q(w^2)` with `deg Q <= 16`, so 17 evaluations in
202
+ `u = w^2` suffice rather than 41 generic values;
203
+ - at `w=0`, the five toroidal layers decouple, giving the exact holdout
204
+ `Z(0) = 272^5 = 1,488,827,973,632`.
205
+
206
+ The exact-integer profile DP remains intentionally guarded to small orders.
207
+ The modular affine-polynomial backend scales exponentially in the
208
+ bipartition order but carries every coefficient through a single sign-vector
209
+ enumeration; exact large profiles can subsequently be reconstructed from
210
+ enough coprime modular runs when a coefficient bound is available.
211
+
212
+ ## Application 2: restricted permutations
213
+
214
+ A binary matrix can encode allowed positions of a permutation. Its permanent
215
+ counts the permitted permutations exactly. In particular, `derangement_matrix`
216
+ returns `J-I`, whose permanent is the subfactorial `!n`.
217
+
218
+ Block diagonal all-ones matrices are also provided as exact Bregman--Minc
219
+ extremizers. A matrix with `k` blocks of order `d` has permanent `(d!)^k`;
220
+ these cases test arbitrary-size results and the SCC factorization path.
221
+
222
+ ## Application 3: Latin transversals, correctly qualified
223
+
224
+ A Latin transversal is a three-dimensional matching and is naturally a
225
+ three-dimensional zero-one permanent—not one ordinary matrix permanent. The
226
+ reference `latin_transversal_count` instead uses the exact coefficient-
227
+ extraction identity
228
+
229
+ `T(L) = sum_S (-1)^(n-|S|) per(A_S)`,
230
+
231
+ where `A_S[i,j]` is one when the symbol `L[i,j]` belongs to `S`. It makes
232
+ `2^n` ordinary binary-permanent calls and is explicitly guarded to small
233
+ orders. Tests include cyclic group tables through order nine and the
234
+ order-four Klein group table, which has eight transversals; even cyclic tables
235
+ having zero must not be generalized to all even Latin squares.
236
+
237
+ ## Application 4: partial matchings and rook polynomials
238
+
239
+ `perm_polynomial.rook_polynomial` returns all partial-matching counts of a
240
+ square binary matrix. Entry `k` counts placements of `k` ones with distinct
241
+ rows and columns. It uses the identity
242
+
243
+ `[x^k] per(J + xA) = (n-k)! r_k`
244
+
245
+ to obtain every coefficient in one coefficient-valued Glynn traversal per
246
+ CRT prime in the portable fallback. The native rook specialization instead
247
+ packs all required pseudo-Mersenne primes with the coefficient dimension, so
248
+ one sign-vector traversal produces every modular vector. Exact fixed-point
249
+ augmented-matrix Bregman--Minc bounds are applied directly to the
250
+ partial-matching counts. An independent characteristic-two Cauchy--Binet
251
+ determinant supplies the coefficient parities, which halves the CRT range and
252
+ selects the unique lift `x` or `x + P` below the certified bound.
253
+
254
+ On the dense SHA-generated order-33 evaluation board, this reduces four
255
+ sequential native CRT traversals (369.26 seconds on the GH200 host CPU) to one
256
+ fused traversal and exact reconstruction (44.00 seconds). Increasing the
257
+ order from 33 to 37 multiplies the fixed-sign state count by 16 and requires
258
+ five packed 27-bit fields rather than four. The fresh order-37 `/1` evaluation
259
+ instance completed in 1,212.81 seconds (20.21 minutes) on the same 64-core
260
+ host.
261
+
262
+ ## Weighted partial matchings and occupancy
263
+
264
+ `weighted_rook_polynomial(W)` supports arbitrary nonnegative integer edge
265
+ weights. Its coefficient `r[k]` sums the products of the edge weights over
266
+ all size-`k` matchings. Disconnected bipartite support components factor
267
+ exactly, including diagonal and star components; the returned tuple still
268
+ contains `n+1` entries with trailing zeros.
269
+
270
+ For nonnegative rational activity `z`, `Z(z)=sum(r[k]*z**k)` is the finite
271
+ monomer-dimer partition function with unit monomer weights. The matching-size
272
+ probability is `r[k]*z**k/Z(z)`. In a source checkout:
273
+
274
+ ```python
275
+ from fractions import Fraction
276
+ from perm_polynomial import weighted_rook_polynomial
277
+ from notes.benchmarks.weighted_matching_statistics import exact_occupancy_statistics
278
+
279
+ coefficients = weighted_rook_polynomial([[1, 2], [3, 4]])
280
+ statistics = exact_occupancy_statistics(coefficients, Fraction(1, 2))
281
+ assert statistics["partition"] == Fraction(17, 2)
282
+ assert statistics["mean"] == Fraction(20, 17)
283
+ assert statistics["variance"] == Fraction(110, 289)
284
+ ```
285
+
286
+ One complete coefficient vector supports many activity queries. The exact
287
+ statistics helper retains rational probabilities and moments; only its
288
+ convenience logarithm is floating point. Its separate belief-propagation
289
+ comparison returns numerical approximations and explicit convergence status.
290
+ Convergence of the message iteration does not certify approximation accuracy.
291
+
292
+ The [public application study](notes/benchmarks/WEIGHTED_MATCHINGS.md) compares
293
+ native coefficients, compiled subset DP, polynomial-valued Ryser, and an
294
+ independent implementation of the Butera-Pernici frontier approach. It also
295
+ compares the cost of reusing coefficients with a compiled exact per-activity
296
+ partition-and-moment query. The study includes dense and structured cases,
297
+ fixed-matrix rescaling, per-method process memory, and retained losses.
298
+
299
+ ## Boundary conventions
300
+
301
+ - A periodic axis must have even length to preserve the parity bipartition.
302
+ - Periodic extent two is rejected: its positive and negative bonds have the
303
+ same endpoints, producing a multigraph that cannot be represented faithfully
304
+ by a zero-one matrix.
305
+ - An odd-volume grid has zero close-packed dimer coverings. The high-level
306
+ count/partition/profile functions return zero (or `(0,)`), while the matrix
307
+ constructors reject its unequal parity classes because the result would not
308
+ be a square biadjacency matrix.
309
+ - Merely having three nontrivial dimensions does not prove nonplanarity; for
310
+ example, `2 x 2 x k` grid graphs are planar.
311
+
312
+ ## Primary references
313
+
314
+ - L. G. Valiant, [*The Complexity of Computing the Permanent*](https://doi.org/10.1016/0304-3975(79)90044-6).
315
+ - P. W. Kasteleyn, [*The Statistics of Dimers on a Lattice. I. The Number of Dimer Arrangements on a Quadratic Lattice*](https://doi.org/10.1016/0031-8914(61)90063-5).
316
+ - D. Cimasoni and N. Reshetikhin, [*Dimers on Surface Graphs and Spin Structures. I*](https://doi.org/10.1007/s00220-007-0302-7).
317
+ - I. Beichl and F. Sullivan, [*Approximating the Permanent via Importance Sampling with Application to the Dimer Covering Problem*](https://doi.org/10.1006/jcph.1998.6149).
318
+ - Y. Huh and S. Oh, [*Dense Dimer Packings in the 3D Cubic Lattice*](https://doi.org/10.1088/1402-4896/ae5605).
319
+ - L. M. Brègman, [*Some Properties of Nonnegative Matrices and Their Permanents*](https://www.mathnet.ru/eng/dan/v211/i1/p27).
320
+ - D. Donovan, K. Johnson, and I. M. Wanless, [*Permanents and Determinants of Latin Squares*](https://doi.org/10.1002/jcd.21418).
321
+ - A. A. Taranenko, [*Multidimensional Permanents and an Upper Bound on the Number of Transversals in Latin Squares*](https://doi.org/10.1002/jcd.21413).
322
+
323
+ These references motivate and validate the application families. They do not
324
+ support the stronger claims that the `4 x 4 x 5` value is previously unknown,
325
+ that every three-dimensional dimer instance requires a permanent algorithm,
326
+ or that a Hofstadter Hamiltonian permanent is automatically a dimer partition
327
+ function. Those claims are intentionally not made here.
@@ -0,0 +1,143 @@
1
+ # Changelog
2
+
3
+ This file records user-visible library, build, and support changes. Historical
4
+ research results remain in their dated notes and session logs.
5
+
6
+ ## Unreleased
7
+
8
+ ### Added
9
+
10
+ - Clean wheel/sdist installation checks covering the installed public API,
11
+ optional Numba, and strict native-extension availability; distribution CI
12
+ for portable and Linux accelerator wheels.
13
+ - `PERM_BUILD_OPENMP=required` to prevent native release builds from silently
14
+ falling back when compilation fails.
15
+ - Reproducible low-rank showcases with explanations of weighted assignments,
16
+ proper complex Gaussian intensity moments, and exact unlabeled profile
17
+ probabilities. Include a grouped-column comparator and an optional
18
+ checkout-only full-rank covariance example with certified approximation.
19
+ - Exact low-rank integer computation in `matrix_permanent`, selected by a
20
+ conservative rank/order/parallelism cost rule. New keyword-only `method`
21
+ choices allow explicit generic or low-rank computation, with a documented
22
+ rank/state budget and `LowRankLimitExceeded` for oversized explicit requests.
23
+ Every CRT prime is preflighted before using the specialization; its CRT bound
24
+ and signed reconstruction are shared with the existing general path.
25
+ - A compiled weighted partial-matching frontier source tool with exact modular
26
+ arithmetic/CRT, reusable column slots, explicit state and native allocation
27
+ limits, independent correctness tests and recorded build provenance. Public
28
+ weighted-rook dispatch is unchanged.
29
+ - A public weighted-matching application study with exact rational occupancy
30
+ distributions, mean and variance, damped monomer-dimer belief propagation,
31
+ an independent Butera-Pernici-style frontier baseline, and a compiled exact
32
+ per-activity partition/moment baseline. Source tools retain fixed-input
33
+ weight scaling, isolated process RSS, raw timings, and replayable CRT records.
34
+ - A general `perm_c_ext.permanent_mod_p` entry point and independent native
35
+ definition/parallel tests, plus a reproducible benchmark across matrix
36
+ families, modulus ranges, and thread counts.
37
+ - Exact `principal_permanent_polynomial` and modular
38
+ `principal_permanent_polynomial_mod` support for the coefficient vector of
39
+ `per(I + xA)`.
40
+ - Exact `weighted_rook_polynomial` support for nonnegative integer matrices,
41
+ including a fused native multi-prime path.
42
+ - A stable `polynomial_backend_capabilities` query for source tools and
43
+ benchmarks that need to report optional native hooks without importing a
44
+ private extension module.
45
+ - Repository support tiers, packaging boundaries, CUDA inventory, and
46
+ contributor guidance.
47
+
48
+ ### Changed
49
+
50
+ - Name the pip distribution `rivin-perm`, preserving all Python import paths.
51
+ Declare metadata in `pyproject.toml`, target Python 3.10 and later, require
52
+ NumPy 1.23.5–2.x at runtime, and build native modules with NumPy 2.x headers
53
+ targeting the NumPy 1.23 API.
54
+ - Build sdists from an explicit public-file allowlist, including reviewed tests;
55
+ omit the three research-only CUDA utilities previously included by name.
56
+ - Reduce weighted-rook dispatch overhead with a dense-row connectivity check
57
+ and bitmask component traversal. Decomposed factors with at most three
58
+ vertices on the smaller side use a bounded exact subset calculation;
59
+ standalone connected inputs retain their CRT backend. Research transcripts
60
+ now distinguish exact small factors from modular factors and replay both.
61
+ - Factor disconnected bipartite support components in `weighted_rook_polynomial`
62
+ and convolve their exact coefficient vectors. Diagonal and star components
63
+ use direct formulas; rectangular components are padded internally and the
64
+ original output length is preserved.
65
+ - General exact integer computations use native Glynn for orders at least
66
+ four with odd moduli in the direct-multiplication range, while retaining
67
+ Ryser for tiny matrices, even moduli, and wider arithmetic. The explicit
68
+ Ryser entry point and compatibility with older extensions are preserved.
69
+ - Cache matrix-independent fixed-point factors used by rook-polynomial bounds
70
+ in bounded caches, reducing repeated small-matrix call overhead.
71
+ - The single-prime backend benchmark now compares general Ryser kernels on
72
+ the same matrix with one thread for each backend.
73
+ - The native rook-polynomial engine can pack up to sixteen same-width
74
+ pseudo-Mersenne prime fields in a fused traversal.
75
+ - Documentation now distinguishes installed public APIs, optional packaged
76
+ accelerators, manually built source tools, validated prototypes, and
77
+ private or archival evidence.
78
+ - Answer-dependent experiment captures and source pins are classified as
79
+ private evaluator evidence regardless of their directory.
80
+ - CUDA sources and selected wrappers are documented as checkout/sdist
81
+ reproducibility material rather than wheel-installed modules.
82
+ - The sdist excludes private checkout-only oracle/integrity tests; its
83
+ remaining test tree is self-contained and skips only unavailable optional
84
+ accelerators or explicitly gated device runs.
85
+
86
+ ### Fixed
87
+
88
+ - Decode the characteristic-three portable result using Python integers before
89
+ bit operations, preserving compatibility with NumPy 1.23.5 as well as 2.x.
90
+ - Make weighted-study workers portable to macOS: report unavailable CPU pinning,
91
+ reject unsupported explicit pinning, and normalize peak RSS using host units.
92
+ - Preserve Schur queue parent-loss and timeout cleanup on macOS with a POSIX
93
+ pipe supervisor, while retaining Linux's direct PDEATHSIG worker path.
94
+ Process identity probes no longer rely on procfs outside Linux or silently
95
+ interpret inspection errors as worker death.
96
+ - Stop the weighted pseudo-Mersenne prime search when its monotone safety
97
+ condition rules out every remaining candidate, preserving selected primes
98
+ and fallback decisions.
99
+ - Correct the full-Haar permanent second moment in research analyses;
100
+ replace unequal and same-sample fitted KS comparisons with common held-out
101
+ distance diagnostics and retain sample/provenance archives for new runs.
102
+ - Replace numerical cycle-path permanent evaluation with a tested analytic
103
+ product, with explicit phase conventions and exact even-midpoint zeros.
104
+ - Revise manuscript novelty claims, statistical interpretations, and
105
+ unsupported finite-field proofs; document withdrawn historical figures
106
+ and remaining empirical verification requirements.
107
+ - Convert NumPy integer scalars to Python integers before exact bounds and
108
+ portable modular arithmetic, preventing fixed-width overflow from silently
109
+ corrupting permanent results.
110
+ - Preserve depth-first finishing order when splitting binary matching graphs
111
+ into strongly connected components, avoiding unnecessary merging of
112
+ independent blocks.
113
+
114
+ ### Verification
115
+
116
+ - Start the September 5 independent verification campaign: standalone Schur
117
+ reconstruction audits and resumable finite-field rank enumeration reproduce
118
+ all eight retained prime-field counts through 23 and the F9 count. Polynomial
119
+ audits replay the retained order-36 interpolation and CRT transcript;
120
+ missing projection-residue arrays are explicitly reported. Reconstruction guarantees
121
+ are explicitly conditional on residues; computation records and fresh
122
+ cross-checks have separately stated coverage.
123
+ - Complete six fresh scalar GPU checks across principal orders 36/38 and rook
124
+ order 40; all agree with retained vectors. Freeze challenges before execution,
125
+ retain source snapshots, and resume from completed calls after interruption.
126
+ - Add a separate full-matrix Schur-45 CUDA Ryser checker with indexed block
127
+ checkpoints and exact input/build manifests. Small GPU and actual-block
128
+ pilots pass; the full independent modular traversal is running. The final
129
+ default suite passes 1,280 tests; future completion must be read from the
130
+ campaign result rather than inferred from a partial run.
131
+ - Principal-permanent and Krawtchouk prototype tests compare against
132
+ independent small-order definitions and seeded random cases.
133
+ - Retained long computations carry canonical hashes and independent
134
+ certificates; their assessments state which checks are recomputed and which
135
+ executions are preserved evidence.
136
+
137
+ ## Historical version note
138
+
139
+ `setup.py` currently reports version `1.0.0`. The repository does not have a
140
+ matching release tag or a pre-existing changelog from which trustworthy
141
+ release dates and earlier version boundaries can be reconstructed. Earlier
142
+ changes are therefore left in git history and dated session logs instead of
143
+ being assigned invented release entries here.
@@ -0,0 +1,140 @@
1
+ # Contributing
2
+
3
+ Contributions should preserve exactness, portable fallbacks, reproducibility,
4
+ and the separation between installed APIs and research evidence. Read the
5
+ [`repository support levels`](REPOSITORY_LAYOUT.md) before
6
+ changing a backend or moving a file.
7
+
8
+ ## Development setup
9
+
10
+ From a source checkout:
11
+
12
+ ```bash
13
+ python -m venv .venv
14
+ . .venv/bin/activate
15
+ python -m pip install --upgrade pip
16
+ python -m pip install -e ".[dev,numba]"
17
+ ```
18
+
19
+ Linux source builds normally attempt the optional OpenMP extensions. To force
20
+ a portable installation while working on fallback behavior:
21
+
22
+ ```bash
23
+ PERM_BUILD_OPENMP=0 python -m pip install -e .
24
+ ```
25
+
26
+ To rebuild the optional extensions in place:
27
+
28
+ ```bash
29
+ python setup.py build_ext --inplace
30
+ ```
31
+
32
+ Do not assume a clean worktree. Inspect `git status --short` before editing,
33
+ preserve unrelated changes, and keep refactors separate from algorithmic
34
+ changes.
35
+
36
+ ## Test matrix
37
+
38
+ The default suite is the required baseline:
39
+
40
+ ```bash
41
+ python -m pytest -q
42
+ ```
43
+
44
+ It exercises portable code, installed extensions when present, source-wrapper
45
+ contracts, independent oracles, and quick retained-value checks. CUDA device
46
+ tests remain skipped unless explicitly enabled.
47
+ [`tests/README.md`](tests/README.md) maps the suite into portable, native,
48
+ source-wrapper, device, prototype, and repository-integrity groups.
49
+
50
+ The source distribution includes the reviewed public test suite. Both
51
+ checkout and unpacked sdist tests must collect and pass independently.
52
+ Distribution checks install real wheels/sdists outside the checkout; follow
53
+ [PACKAGING.md](PACKAGING.md) before preparing a pip release.
54
+
55
+ For generic or polynomial native work, require rather than silently skip the
56
+ extensions:
57
+
58
+ ```bash
59
+ python setup.py build_ext --inplace
60
+ PERM_REQUIRE_NATIVE_TESTS=1 \
61
+ python -m pytest -q tests/test_generic_native.py tests/test_polynomial_native.py
62
+ ```
63
+
64
+ Run the smallest focused module while iterating, then the full default suite.
65
+ Finish documentation and source changes with:
66
+
67
+ ```bash
68
+ git diff --check
69
+ ```
70
+
71
+ ### CUDA tests
72
+
73
+ CUDA changes require the opt-in device modules on a compatible host:
74
+
75
+ ```bash
76
+ PERM_RUN_CUDA_TESTS=1 \
77
+ PERM_CUDA_TEST_ARCH=native \
78
+ PERM_CUDA_ARCH=native \
79
+ python -m pytest -q \
80
+ tests/test_cuda_device_binary.py \
81
+ tests/test_cuda_device_modular.py \
82
+ tests/test_cuda_device_parallel.py \
83
+ tests/test_cuda_device_schur_glynn.py \
84
+ tests/test_cuda_device_utilities.py
85
+ ```
86
+
87
+ The order-41 and order-47 regressions require
88
+ `PERM_RUN_CUDA_LONG_TESTS=1` and `PERM_RUN_CUDA_FRONTIER_TESTS=1`,
89
+ respectively. They are deliberate long runs, not ordinary CI. See
90
+ [`CUDA.md`](CUDA.md#device-tests) for details.
91
+
92
+ ## Exactness requirements
93
+
94
+ An optimized exact backend needs more than agreement on one large answer.
95
+ Include, as applicable:
96
+
97
+ - definition-level exhaustive tests at small order;
98
+ - seeded random comparisons with an independent implementation;
99
+ - edge cases for empty, singular, signed, sparse, and oversized inputs;
100
+ - a rigorous coefficient or permanent bound;
101
+ - CRT range, parity, divisibility, or centered-lift checks;
102
+ - deterministic instance and output hashes for retained long runs; and
103
+ - a foreign-modulus or otherwise independent holdout for expensive results.
104
+
105
+ Unsafe arithmetic ranges must be rejected or sent to a proven fallback. A
106
+ benchmark speedup is not permission to weaken an input contract.
107
+
108
+ ## Benchmarks and retained results
109
+
110
+ Benchmark scripts should have a quick safe default, deterministic seeds or
111
+ instance labels, machine-readable output, repeat consistency checks, and an
112
+ explicit opt-in such as `--allow-large` for expensive exponential work.
113
+
114
+ When retaining a result, record:
115
+
116
+ - the exact command and relevant environment variables;
117
+ - CPU/GPU identity, compiler, architecture, and thread count;
118
+ - source and executable hashes when binaries matter;
119
+ - kernel and wall time separately when useful;
120
+ - the complete output or a canonical output hash; and
121
+ - the independent checks that passed.
122
+
123
+ Do not commit generated shared objects, compiler output, logs, or LaTeX
124
+ intermediates. Curated captures and figures are evidence only when their
125
+ provenance is documented.
126
+
127
+ ## Documentation and compatibility
128
+
129
+ - Update README usage when a Tier-1 API changes.
130
+ - Update `REPOSITORY_LAYOUT.md` when support or packaging status changes.
131
+ - Update `CUDA.md` when a CUDA contract, build, or test gate changes.
132
+ - Add user-visible changes to the Unreleased section of `CHANGELOG.md`.
133
+ - Preserve current import paths and documented return conventions unless a
134
+ migration and compatibility plan is explicit.
135
+ - Treat native/CUDA file paths as interfaces: build files, tests, verifiers,
136
+ and retained hashes currently refer to them directly.
137
+
138
+ Historical session logs describe what happened at a date; do not silently
139
+ rewrite them to look current. Put current instructions in README, this guide,
140
+ or an adjacent assessment instead.