diff --git a/research/nonreduced-tensor-recognition/INPUTS.json b/research/nonreduced-tensor-recognition/INPUTS.json new file mode 100644 index 0000000000..fc3d42914f --- /dev/null +++ b/research/nonreduced-tensor-recognition/INPUTS.json @@ -0,0 +1,26 @@ +[ + { + "local": "source/nucleus_source.py", + "repository": "aliromman/integer-mult-bounds", + "commit": "3d5f42dc084b8c9e1ad73318458751e851752a10", + "path": "research/etale-tensor-recognition/recognize.py", + "git_blob": "f1bb574dad4f14666b16f50c079216d9c63fea0a", + "sha256": "17eaabfeef8f561ed90d547f6c6c13e249f5a0a8a24028c86a96e56f0301e110" + }, + { + "local": "source/table_source.py", + "repository": "apoursh/integer-mult-bounds", + "commit": "c09b7a57f81f941b7f9ee6882172eb21a683ded1", + "path": "research/frobenius-algebra-recognition/recognize.py", + "git_blob": "875be1c29d9afc35c975073a59c9f959253fc53f", + "sha256": "21bc6bb539757726c23fa63105e81c70f1723937ba3b98a905a22994295e1521" + }, + { + "local": "source/table-proof.md", + "repository": "apoursh/integer-mult-bounds", + "commit": "c09b7a57f81f941b7f9ee6882172eb21a683ded1", + "path": "research/frobenius-algebra-recognition/PROOF.md", + "git_blob": "0a0b7d1fe48576be453df5aa5de0cc66463352fa", + "sha256": "726d9c294f0b4b9085e3994ffb8c93737822b78d6f5d0c63f81e36b718419e28" + } +] diff --git a/research/nonreduced-tensor-recognition/README.md b/research/nonreduced-tensor-recognition/README.md new file mode 100644 index 0000000000..826c6b0107 --- /dev/null +++ b/research/nonreduced-tensor-recognition/README.md @@ -0,0 +1,221 @@ +# Recognize multiplication tensors with nilpotents + +This package recognizes multiplication in any finite-dimensional commutative +unital algebra from a square tensor over a prime field, up to independent +changes of basis in its two inputs and output. It returns the algebra table +and all three changes of basis. The new test checks regularity of both nucleus +modules when the algebra has nilpotents. + +Composing this reconstruction with apoursh's PR #5502 recognizer gives a +necessary-and-sufficient test, recovered heights and explicit transporters for + + F_p[x_1,...,x_r] / (x_i^(p^h_i)), h_i >= 1, + +from an arbitrary square tensor. This closes the three-factor reconstruction +handoff in that PR. An empty height list represents F_p. The procedure is +deterministic polynomial time for each fixed prime p. There is no new kappa, +physical supplier or general tensor-isomorphism algorithm. + +## Exact sources and interface + +Canonical base: `32890bc5de034ab8fc93ac88ead7279a6a416d04`, priority revision 7. +The [scoped claim](https://github.com/CrocSwap/integer-mult-bounds/discussions/1166#discussioncomment-19174752) +follows the completed supplied-table and finite-etale claims. Full cached +coverage at entry included 398 open PRs, 4,820 Discussion records and every +tracked local research note. Relevant all-state PRs and canonical handoffs +were also read. These checks do not prove global novelty. + +`recognize.py` exports `recognize_commutative(t,p)` and +`recognize_frobenius(t,p)`. Both return `(certificate, None)` on acceptance +or `(None, reason)` on mathematical rejection. Invalid shapes, composite p +and entries outside the canonical residues raise `ValueError`. Use +`t[i][j][k]` for the coefficient of output k on input basis vectors i,j. +The input dimension must be positive. Maps U,V,W are matrices with basis +images in their columns, and satisfy `t(Ua,Vb)=W*mu(a,b)`. + +Three source files are copied without changes and authenticated before use. +`INPUTS.json` records their original repository, path, commit, Git object and +SHA256. No network or unmerged Git object is needed for a replay. + +- Ali Romman's [PR #5525](https://github.com/CrocSwap/integer-mult-bounds/pull/5525), + [exact matrix and nucleus routines](https://github.com/aliromman/integer-mult-bounds/blob/3d5f42dc084b8c9e1ad73318458751e851752a10/research/etale-tensor-recognition/recognize.py), + supplies the frozen arithmetic, nucleus solver and fixed-point projectors. + Its finite-etale acceptance test is not used here. +- apoursh's [PR #5502](https://github.com/CrocSwap/integer-mult-bounds/pull/5502), + [exact algebra classifier](https://github.com/apoursh/integer-mult-bounds/blob/c09b7a57f81f941b7f9ee6882172eb21a683ded1/research/frobenius-algebra-recognition/recognize.py) + and [proof](https://github.com/apoursh/integer-mult-bounds/blob/c09b7a57f81f941b7f9ee6882172eb21a683ded1/research/frobenius-algebra-recognition/PROOF.md), + supplies the final Frobenius-truncated class test and monomial transporter. + Its theorem is a credited premise of that composed class result. +- Nick Petalas's [PR #5252](https://github.com/CrocSwap/integer-mult-bounds/pull/5252) + supplies the nucleus and finite-etale projectors, with David Ganz's atlas + and the retained predecessor lineage. Finite algebra decomposition and + the nilpotent form of Nakayama's lemma are classical. + +## Recognition criterion + +Let t:U x V -> W be bilinear, with all three dimensions equal to n>0 over +F_p. Compute the left nucleus by linear equations: + + B = {(a,c) in End(U) x End(W): t(au,v)=c t(u,v) for all u,v}. + +It is a unital algebra under componentwise composition. Require: + +1. dim B=n and both projections rho:B->End(U), lambda:B->End(W) are injective; +2. B is commutative, and t has zero right kernel; +3. for every primitive component e_i of B, with residue field degree d_i, + + dim(e_i U / e_i J U) = d_i, + dim(e_i W / e_i J W) = d_i, (1) + + where J is the nilpotent radical of B. + +These conditions are necessary and sufficient for t to be isotopic to +commutative unital multiplication. They construct the transporter below. +All required spaces, including the two quotient dimensions, come from linear +algebra on the supplied tensor. No invertible slice is assumed or searched +by enumeration of vectors. + +## Radical and primitive components + +Use the faithful matrix image lambda(B) as coordinates for B. Since B is +commutative, F:b->b^p is an F_p-linear map. Let E be the least nonnegative +integer with p^E>=n. Then + + J=ker(F^E), J^n=0. (2) + +For completeness, nilpotent elements form an ideal in a commutative algebra. +A finite basis of nilpotent generators shows that this ideal is nilpotent. +Its power chain strictly decreases until zero: equality at two consecutive +powers would persist at every later power. Its dimension is at most n-1, +so its nth power vanishes. Thus (2) kills every nilpotent, and it kills no +other element. The reduced finite algebra B/J is a product of finite fields. + +The fixed subalgebra ker(F-I) is F_p^s, one scalar for each local component. +To see surjectivity onto these scalar tuples, lift a tuple to b in B. +Then F(b)-b lies in J, so F^E(b) is fixed and has the same residue tuple. +A fixed nilpotent is zero, which proves injectivity. Hence a basis of this +fixed space separates the components. For each fixed element b and scalar +c in F_p, the polynomial + + 1-(b-c*1)^(p-1) + +is its exact scalar-eigenspace projector. Repeatedly multiply the current +projectors by these polynomials, and discard zero products. The surviving +projectors are the primitive e_i. Their residue degrees are +`d_i=dim(e_i B)-dim(e_i J)`. This argument also covers nonreduced B; it does +not require injective Frobenius. + +## Why the top test is sufficient + +For M=U or W, the quotient e_i M/e_i J M is a vector space over the field +B_i/e_i J, of F_p-dimension d_i. Condition (1) says it is one-dimensional +over that field. Choose any vector v_i in e_i M outside e_i J M. A projected +coordinate column supplies one by linear elimination. It generates this +quotient. Summing these vectors over all components gives m_0 with + + M=B m_0 + J M. + +Pass to Q=M/B m_0. This says Q=JQ; iteration through J^n=0 gives Q=0. +Thus m_0 is cyclic. Since dim B=dim M=n, the surjection b->b m_0 is an +isomorphism. This proves regularity, including its constructive part. +Apply the argument separately to U and W to obtain u_0 and w_0. + +Define + + alpha(b)=rho(b)u_0, gamma(b)=lambda(b)w_0, + Phi(v)=t(u_0,v). + +The maps alpha and gamma are isomorphisms. If Phi(v)=0, the nucleus identity +and cyclicity of u_0 give t(U,v)=0. The zero right kernel gives v=0. Equal +dimensions make Phi an isomorphism. Put beta=Phi^-1 gamma. For all a,b in B, + + t(alpha(a), beta(b)) = lambda(a) gamma(b) = gamma(ab). (3) + +Equation (3) gives U,V,W and the multiplication table of B. + +For necessity, consider ordinary multiplication in a commutative unital A. +For a nucleus pair (a,c), setting the first input to 1 gives c(v)=a(1)v; +setting the second to 1 then gives a(u)=c(u)=a(1)u. Thus the nucleus is A, +with both regular actions. Its dimension, faithful projections, zero right +kernel and top dimensions satisfy every test. Independent basis changes +preserve these facts. This proves both directions of the criterion. + +## A necessary nonregular control + +Set A=F_p[x,y]/(x,y)^2, with basis 1,x,y. Its dual A* is a faithful A-module +of dimension three, but its top A*/J A* has dimension two. The tensor + + A x A* -> A*, (a,f) -> a*f + +has a commutative three-dimensional left nucleus, faithful projections and +zero right kernel. Its first module is regular; its output module is not. +It fails exactly the output-top test. Interchanging its inputs gives an +input-top failure. Thus matching dimension and faithfulness do not replace +(1). The checks include both tensors over F2, F3 and F5. + +## Compose the Frobenius-truncated class transporter + +The recovered algebra B is isomorphic to the left nucleus. If the original +tensor is isotopic to the target class, necessity above identifies B with +that target algebra, so the pinned PR #5502 classifier accepts. Conversely, +if it accepts, its monomial matrix C satisfies + + mu_B(Ca,Cb)=C mu_model(a,b). + +Replace the maps in (3) by U C, V C, W C. These are the required independent +basis changes from the standard truncated-polynomial model to the original +tensor. The verifier checks every basis product of this composed certificate +without using the nucleus test, radical dimensions or classifier decision. +This gives a necessary-and-sufficient recognition procedure for the class. + +All stages are deterministic polynomial operations at fixed p: the nucleus +uses n^3 equations in 2n^2 unknowns; Frobenius and its kernels use matrix +powering and elimination; the projector step ranges over p scalars; and +both top tests and transporter inversions use linear algebra. The final +classifier has the fixed-prime guarantee in its pinned proof. Trial-division +primality and the loop over p do not give a polynomial bound in log p when +the characteristic varies. No extension-field input format is supplied. + +## Verification and limitations + +Run `python3.12 -B scripts/research_package.py --package research/nonreduced-tensor-recognition`. +The standard library suffices. The registered normal and optimized commands +use isolated, warning-strict Python and print `PASS_NONREDUCED_TENSOR_RECOGNITION`. + +The complete 256 binary 2x2x2 tensor domain agrees with independently generated +GL2^3 orbits. The split, field and dual-number orbits have sizes 108,12,54. +Exactly 174 tensors pass general commutative recognition and 54 pass the +Frobenius-truncated class test. The oracle uses literal products and an +explicit two-by-two inverse, independently of the nucleus acceptance path. + +Forty further presentations cover primes 2,3,5,7,13, dimensions through nine, +mixed heights, nonsplit local residue fields, non-Gorenstein algebras and +multiple local components. In total, 1,736 transported basis products and +76 controls pass. The controls include six nonregular module presentations, +a noncommutative matrix algebra, 48 altered transporter entries, six invalid +inputs and three source-byte changes. All verification conditions use +explicit exceptions and remain active under `-O`. + +The written proof gives the general statement; these finite checks do not +replace its review. The source class theorem is credited and its full prior +suite is not rerun. Independent human or proof-kernel acceptance, extension +fields, a selected cold replay, physical coefficient preparation, native +realization, precision, tape costs and supplier admission remain outside +this package. General literature priority is not claimed. + +## Open next questions + +- Add an explicit extension-field input layer. Start by distinguishing q-power + Frobenius for decomposition from p-power Frobenius for the height flag. + Field arithmetic and semilinear kernels require a separate contract. +- Given two accepted general commutative tensors, decide whether their recovered + algebras are isomorphic. The present result recovers tables and transports; + it does not solve general commutative-algebra isomorphism. +- Apply the recovered height list to the pinned determinant-character formula + of PR #5459. Verify the tensor action convention before using orbit counts. + This algebraic bridge supplies no physical implementation or paid budget. + +New regular-module reconstruction, composition and checks: Ali Romman +(@aliromman), with substantial OpenAI Codex assistance. Source classifier: +apoursh (@apoursh). Retain Nick Petalas (@petalas), David Ganz and the cited +algebra and atlas lineage. Apache-2.0. diff --git a/research/nonreduced-tensor-recognition/SOURCES.json b/research/nonreduced-tensor-recognition/SOURCES.json new file mode 100644 index 0000000000..d24f68acb8 --- /dev/null +++ b/research/nonreduced-tensor-recognition/SOURCES.json @@ -0,0 +1,44 @@ +{ + "schema_version": 1, + "base_commit": "32890bc5de034ab8fc93ac88ead7279a6a416d04", + "files": [ + { + "path": "certificates/selected-result.json", + "usage": "provenance", + "git_blob": "fc3d4ba105e1f097d0d7d051a308281e86ab1446", + "sha256": "66974efdced4083c00025f97d95d83909a226cdf801edb9301c195c6641f6903" + }, + { + "path": "docs/research/agent-task-catalog.json", + "usage": "provenance", + "git_blob": "f126dd2e2dad2b669e75d1d0d0bd05c600725602", + "sha256": "f7a48bfa2659b88378282a8eee7d9fe52cdf505594124c4d1686b58510fffd72" + } + ], + "external_provenance": [ + { + "local": "source/nucleus_source.py", + "repository": "aliromman/integer-mult-bounds", + "commit": "3d5f42dc084b8c9e1ad73318458751e851752a10", + "path": "research/etale-tensor-recognition/recognize.py", + "git_blob": "f1bb574dad4f14666b16f50c079216d9c63fea0a", + "sha256": "17eaabfeef8f561ed90d547f6c6c13e249f5a0a8a24028c86a96e56f0301e110" + }, + { + "local": "source/table_source.py", + "repository": "apoursh/integer-mult-bounds", + "commit": "c09b7a57f81f941b7f9ee6882172eb21a683ded1", + "path": "research/frobenius-algebra-recognition/recognize.py", + "git_blob": "875be1c29d9afc35c975073a59c9f959253fc53f", + "sha256": "21bc6bb539757726c23fa63105e81c70f1723937ba3b98a905a22994295e1521" + }, + { + "local": "source/table-proof.md", + "repository": "apoursh/integer-mult-bounds", + "commit": "c09b7a57f81f941b7f9ee6882172eb21a683ded1", + "path": "research/frobenius-algebra-recognition/PROOF.md", + "git_blob": "0a0b7d1fe48576be453df5aa5de0cc66463352fa", + "sha256": "726d9c294f0b4b9085e3994ffb8c93737822b78d6f5d0c63f81e36b718419e28" + } + ] +} diff --git a/research/nonreduced-tensor-recognition/TASK.json b/research/nonreduced-tensor-recognition/TASK.json new file mode 100644 index 0000000000..fb83b8b271 --- /dev/null +++ b/research/nonreduced-tensor-recognition/TASK.json @@ -0,0 +1,95 @@ +{ + "schema_version": 1, + "task_id": "formal-interface-lemma", + "priority_revision": 7, + "base_commit": "32890bc5de034ab8fc93ac88ead7279a6a416d04", + "original_card": { + "id": "formal-interface-lemma", + "question": "Can one uncovered interface implication be formalized with exact caller and certificate bindings?", + "starting_points": [ + "docs/research/research-family-status.md", + "certificates/selected-result.json", + "research/gate-operand-parity-lean/README.md", + "research/staged544-certified-endpoint/README.md", + "research/unconditional-batched-crt/README.md" + ], + "methods": [ + "Formalize a missing boundary lemma identified by the selected review.", + "Audit a formal theorem-to-certificate binding independently." + ], + "acceptance_gate": "State the exact formalized proposition and remaining assumptions; avoid duplicate arithmetic formalizations already present.", + "first_step": { + "command": [ + "python3", + "-B", + "scripts/research_baseline.py" + ], + "expected": "PASS_RESEARCH_BASELINE_BINDINGS_ONLY", + "inspect": "Inventory the gate-operand parity lemma (#3807) and certified endpoint arithmetic (#3857). Neither is a full multiplication proof. For #4100, audit the compact-switch statement and its hypotheses before formalizing it.", + "scope": "Source-binding orientation only; no scientific replay, theorem acceptance or supplier admission. Budget any heavy replay separately." + }, + "first_experiment": "Write one missing statement with quantified hypotheses, actual caller and existing coverage. A compact-switch lemma is a candidate only after its mathematical contract is reviewed. Establish a bounded proof plan before a separately budgeted proof-assistant build; do not rerun large Lean jobs as orientation.", + "bottleneck_link": "A missing interface proof can reduce the conditional gap even if it does not change kappa.", + "allowed_variations": [ + "Narrow to one formal lemma or one theorem-to-certificate binding." + ], + "scope_change_triggers": [ + "Formalizing already-covered arithmetic and claiming the full multiplication theorem." + ], + "overlap_terms": [ + "hyperplane end", + "formal interface", + "Lean binding", + "3807", + "3857", + "4100", + "gate-operand-parity-lean", + "staged544-certified-endpoint", + "unconditional-batched-crt" + ], + "existing_checks": [ + { + "scope": "PR #3807: scoped gate-operand parity formalization.", + "evidence": "research/gate-operand-parity-lean/README.md", + "remaining": "Exact theorem-to-caller binding and genuinely uncovered interface lemmas.", + "status": "author-checked", + "source_commit": "4c3530f42a6abcc48e7a6bcffd6c901911dc8269", + "selected_record_sha256": "bb2eaa58647b353f88d0aec2eafb9d30df1be53589231b23e12ceef3e4bad129" + }, + { + "scope": "PR #3857: certified real-power moment/endpoint arithmetic.", + "evidence": "research/staged544-certified-endpoint/README.md", + "remaining": "Uniform algorithmic implications, not another endpoint digit.", + "status": "author-checked", + "source_commit": "ef4315b8caa605e47a2515004845eb2d2ed71a86", + "selected_record_sha256": "bb2eaa58647b353f88d0aec2eafb9d30df1be53589231b23e12ceef3e4bad129" + }, + { + "scope": "PR #4100: compact-switch formalization proposed as future validation.", + "evidence": "research/unconditional-batched-crt/README.md", + "remaining": "Audit the statement and assumptions before a formal proof campaign.", + "status": "author-checked", + "source_commit": "337d04939571a637aeab54a0f58f54fde811663a", + "selected_record_sha256": "bb2eaa58647b353f88d0aec2eafb9d30df1be53589231b23e12ceef3e4bad129" + } + ] + }, + "scope_status": "changed", + "actual_scope": "Recover regular nucleus actions and compatible factor maps from nonreduced tensors, then compose PR #5502 table recognition.", + "scope_change_reason": "Take the explicit unclaimed tensor-reconstruction handoff in PR #5502, following the completed finite-etale scope in PR #5525.", + "bottleneck_link": "This removes the isotopy promise before applying the Frobenius algebra class and character methods; it is an algebraic side result.", + "budget_minutes": 60, + "stop_rule": "Stop after the recognition proof, composed transporters and bounded exact checks. Do not infer general algebra isomorphism or supplier costs.", + "outcome": "completed", + "evidence": "README.md and registered normal/optimized checks in verify.py.", + "author": "Ali Romman", + "ai_assistance": "Substantial OpenAI Codex assistance.", + "open_questions": [ + { + "question": "Can the input layer support explicit extension fields?", + "why_it_matters": "This would extend the class test beyond prime-field input.", + "first_step": "Separate q-power decomposition from semilinear p-power height kernels.", + "blocked_by": "No extension-field input contract is supplied." + } + ] +} diff --git a/research/nonreduced-tensor-recognition/VERIFY.json b/research/nonreduced-tensor-recognition/VERIFY.json new file mode 100644 index 0000000000..ef31987f83 --- /dev/null +++ b/research/nonreduced-tensor-recognition/VERIFY.json @@ -0,0 +1,34 @@ +{ + "schema_version": 1, + "status": "ready", + "python": "3.12", + "requirements": [], + "inputs": [], + "commands": [ + { + "argv": [ + "{python}", + "-I", + "-W", + "error", + "-B", + "{package}/verify.py" + ], + "timeout_seconds": 120, + "checks": "Authenticate sources; classify all binary 2x2x2 tensors; verify nonreduced transporters and rejection controls." + }, + { + "argv": [ + "{python}", + "-I", + "-W", + "error", + "-B", + "-O", + "{package}/verify.py" + ], + "timeout_seconds": 120, + "checks": "Authenticate sources; classify all binary 2x2x2 tensors; verify nonreduced transporters and rejection controls." + } + ] +} diff --git a/research/nonreduced-tensor-recognition/recognize.py b/research/nonreduced-tensor-recognition/recognize.py new file mode 100644 index 0000000000..7bb9208a4e --- /dev/null +++ b/research/nonreduced-tensor-recognition/recognize.py @@ -0,0 +1,118 @@ +"""Recover commutative multiplication from a square prime-field tensor. + +The new step checks local module tops before constructing cyclic vectors. +The pinned sources supply matrix routines and the final algebra classifier. +Ali Romman with substantial OpenAI Codex assistance. Apache-2.0. +""" +import hashlib +import importlib.util +import json +from math import isqrt +from pathlib import Path + +HERE = Path(__file__).resolve().parent + + +def load_source(name): + row = next(r for r in json.loads((HERE / 'INPUTS.json').read_text()) + if r['local'] == 'source/' + name + '.py') + path = HERE / row['local'] + data = path.read_bytes() + blob = b'blob ' + str(len(data)).encode() + b'\0' + data + if hashlib.sha256(data).hexdigest() != row['sha256'] or hashlib.sha1(blob).hexdigest() != row['git_blob']: + raise ValueError('source authentication failed: ' + name) + spec = importlib.util.spec_from_file_location(name, path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +N = load_source('nucleus_source') +TABLE = load_source('table_source') + + +def cyclic_vector(action, Y, parts, radical, p): + """Check each simple top and lift one generator from each component.""" + n = len(Y) + result = [0] * n + dimensions = [] + for e in parts: + ec = N.coordinates(Y, e, p) + projection = N.combine(action, ec, p) + component = [N.coordinates(Y, N.multiply(e, a, p), p) for a in Y] + residue_degree = N.rank(radical + component, p) - len(radical) + j_columns = [] + for r in radical: + matrix = N.multiply(projection, N.combine(action, r, p), p) + j_columns.extend(map(list, zip(*matrix))) + j_rank = N.rank(j_columns, p) + top = N.rank(projection, p) - j_rank + dimensions.append((residue_degree, top)) + if top != residue_degree: + return None, dimensions + vector = next((list(v) for v in zip(*projection) + if N.rank(j_columns + [list(v)], p) > j_rank), None) + N.require(vector is not None, 'nonzero simple top') + result = [(x + y) % p for x, y in zip(result, vector)] + return result, dimensions + + +def recognize_commutative(t, p): + """Return a complete transporter or a mathematical rejection reason. + + Inputs use canonical residues. Malformed inputs raise ValueError. + """ + N.require(type(p) is int and p >= 2 and + not any(p % d == 0 for d in range(2, isqrt(p)+1)), 'prime field required') + N.require(isinstance(t, (list, tuple)) and len(t) > 0, 'nonempty tensor') + n = len(t) + N.require(all(isinstance(row, (list, tuple)) and len(row) == n and + all(isinstance(v, (list, tuple)) and len(v) == n for v in row) + for row in t), 'tensor shape') + N.require(all(type(a) is int and 0 <= a < p for row in t for v in row for a in v), + 'canonical prime-field entries') + X, Y = N.nucleus(t, p) + if len(Y) != n: + return None, 'nucleus dimension' + if any(N.rank([N.flat(a) for a in basis], p) != n for basis in [X,Y]): + return None, 'nucleus projection' + if any(N.multiply(a,b,p) != N.multiply(b,a,p) for a in Y for b in Y): + return None, 'noncommutative nucleus' + right = N.transpose([[t[i][j][k] for i in range(n) for k in range(n)] for j in range(n)]) + if N.rank(right,p) != n: + return None, 'right kernel' + frobenius = N.transpose([N.coordinates(Y,N.power(a,p,p),p) for a in Y]) + exponent, bound = 0, 1 + while bound < n: + exponent += 1 + bound *= p + radical = N.nullspace(N.power(frobenius,exponent,p),p) + parts = N.primitive_parts(Y,frobenius,p) + u0, u_top = cyclic_vector(X,Y,parts,radical,p) + if u0 is None: + return None, 'input top multiplicity' + w0, w_top = cyclic_vector(Y,Y,parts,radical,p) + if w0 is None: + return None, 'output top multiplicity' + U = N.transpose([N.matvec(a,u0,p) for a in X]) + W = N.transpose([N.matvec(a,w0,p) for a in Y]) + phi = N.transpose([N.evaluate(t,u0,v,p) for v in N.identity(n)]) + N.require(all(N.rank(a,p)==n for a in [U,W,phi]), 'regularity construction failed') + V = N.multiply(N.inverse(phi,p),W,p) + table = [[N.coordinates(Y,N.multiply(a,b,p),p) for b in Y] for a in Y] + return dict(table=table,U=U,V=V,W=W,radical_dimension=len(radical), + residue_degrees=sorted(x[0] for x in w_top), + input_tops=u_top,output_tops=w_top), None + + +def recognize_frobenius(t, p): + """Compose the recovered table with apoursh's pinned class recognizer.""" + certificate, reason = recognize_commutative(t,p) + if certificate is None: + return None, reason + algebra = TABLE.recognize(p,certificate['table']) + if algebra['status'] != 'accepted': + return None, 'algebra class: ' + algebra['reason'] + C = N.transpose(algebra['transporter']) + return dict(heights=algebra['heights'], + **{key:N.multiply(certificate[key],C,p) for key in ['U','V','W']}), None diff --git a/research/nonreduced-tensor-recognition/source/nucleus_source.py b/research/nonreduced-tensor-recognition/source/nucleus_source.py new file mode 100644 index 0000000000..f1bb574dad --- /dev/null +++ b/research/nonreduced-tensor-recognition/source/nucleus_source.py @@ -0,0 +1,189 @@ +"""Recognize finite etale multiplication in exact prime-field fixtures. + +Ali Romman with substantial OpenAI Codex assistance. Apache-2.0. +""" + + +def require(condition, message): + if not condition: + raise ValueError(message) + + +def rref(rows, p): + a = [list(row) for row in rows] + pivots = [] + k = 0 + if not a: + return a, pivots + for j in range(len(a[0])): + i = next((i for i in range(k, len(a)) if a[i][j] % p), None) + if i is None: + continue + a[k], a[i] = a[i], a[k] + inv = pow(a[k][j], -1, p) + a[k] = [x * inv % p for x in a[k]] + for i in range(len(a)): + if i != k: + c = a[i][j] + a[i] = [(x - c * y) % p for x, y in zip(a[i], a[k])] + pivots.append(j) + k += 1 + if k == len(a): + break + return a, pivots + + +def rank(a, p): + return len(rref(a, p)[1]) + + +def nullspace(a, p): + reduced, pivots = rref(a, p) + n = len(a[0]) + out = [] + for j in range(n): + if j in pivots: + continue + v = [0] * n + v[j] = 1 + for k, i in enumerate(pivots): + v[i] = -reduced[k][j] % p + out.append(v) + return out + + +def transpose(a): + return [list(c) for c in zip(*a)] + + +def flat(a): + return [x for row in a for x in row] + + +def identity(n): + return [[int(i == j) for j in range(n)] for i in range(n)] + + +def multiply(a, b, p): + return [[sum(x * y for x, y in zip(r, c)) % p for c in zip(*b)] for r in a] + + +def matvec(a, v, p): + return [sum(x * y for x, y in zip(row, v)) % p for row in a] + + +def add(a, b, p, scale=1): + return [[(x + scale * y) % p for x, y in zip(r, s)] for r, s in zip(a, b)] + + +def inverse(a, p): + n = len(a) + b, pivots = rref([r + s for r, s in zip(a, identity(n))], p) + require(pivots[:n] == list(range(n)), 'singular matrix') + return [r[n:] for r in b] + + +def power(a, k, p): + out = identity(len(a)) + while k: + if k & 1: + out = multiply(out, a, p) + a = multiply(a, a, p) + k //= 2 + return out + + +def combine(basis, coefficients, p): + n = len(basis[0]) + out = [[0] * n for _ in range(n)] + for c, a in zip(coefficients, basis): + out = add(out, a, p, c) + return out + + +def coordinates(basis, target, p): + columns = [flat(a) for a in basis] + n = len(columns) + a, pivots = rref([list(row) + [v] for row, v in zip(zip(*columns), flat(target))], p) + require(pivots == list(range(n)), 'target outside independent matrix basis') + return [a[i][-1] for i in range(n)] + + +def evaluate(t, u, v, p): + n = len(t) + return [sum(u[i] * v[j] * t[i][j][k] for i in range(n) for j in range(n)) % p for k in range(n)] + + +def nucleus(t, p): + n = len(t) + equations = [] + for i in range(n): + for j in range(n): + for k in range(n): + row = [0] * (2 * n * n) + for a in range(n): + row[a * n + i] = (row[a * n + i] + t[a][j][k]) % p + for b in range(n): + row[n * n + k * n + b] = (row[n * n + k * n + b] - t[i][j][b]) % p + equations.append(row) + kernel = nullspace(equations, p) + X = [[[v[i * n + j] for j in range(n)] for i in range(n)] for v in kernel] + Y = [[[v[n * n + i * n + j] for j in range(n)] for i in range(n)] for v in kernel] + return X, Y + + +def primitive_parts(Y, frobenius, p): + n = len(Y) + fixed = nullspace(add(frobenius, identity(n), p, -1), p) + parts = [identity(n)] + for c in fixed: + matrix = combine(Y, c, p) + refined = [] + for h in parts: + for scalar in range(p): + shifted = add(matrix, identity(n), p, -scalar) + block = multiply(h, add(identity(n), power(shifted, p - 1, p), p, -1), p) + if any(flat(block)): + refined.append(block) + parts = refined + return parts + + +def transporter(t, X, Y, parts, p): + n = len(t) + u0, w0 = [0] * n, [0] * n + for e in parts: + ex = combine(X, coordinates(Y, e, p), p) + u = next(c for c in zip(*ex) if any(c)) + w = next(c for c in zip(*e) if any(c)) + u0 = [(x + y) % p for x, y in zip(u0, u)] + w0 = [(x + y) % p for x, y in zip(w0, w)] + U = transpose([matvec(x, u0, p) for x in X]) + W = transpose([matvec(y, w0, p) for y in Y]) + phi = transpose([evaluate(t, u0, v, p) for v in identity(n)]) + V = inverse(multiply(inverse(W, p), phi, p), p) + require(rank(U, p) == n, 'cyclic input construction') + table = [[coordinates(Y, multiply(a, b, p), p) for b in Y] for a in Y] + return dict(table=table, U=U, V=V, W=W, degrees=sorted(rank(e, p) for e in parts)) + + +def recognize(t, p): + require(p in (2, 3, 5, 7), 'unsupported fixture field') + n = len(t) + require(n > 0 and all(len(row) == n and all(len(v) == n for v in row) for row in t), 'tensor shape') + require(all(type(x) is int and 0 <= x < p for row in t for v in row for x in v), 'field entries') + X, Y = nucleus(t, p) + if len(X) != n: + return None, 'nucleus dimension' + if rank([flat(a) for a in X], p) != n or rank([flat(a) for a in Y], p) != n: + return None, 'nucleus projection' + if any(multiply(a, b, p) != multiply(b, a, p) for a in Y for b in Y): + return None, 'noncommutative nucleus' + frobenius = transpose([coordinates(Y, power(a, p, p), p) for a in Y]) + if rank(frobenius, p) != n: + return None, 'nilpotent nucleus' + right = transpose([[t[i][j][k] for i in range(n) for k in range(n)] for j in range(n)]) + if rank(right, p) != n: + return None, 'right kernel' + parts = primitive_parts(Y, frobenius, p) + return transporter(t, X, Y, parts, p), None diff --git a/research/nonreduced-tensor-recognition/source/table-proof.md b/research/nonreduced-tensor-recognition/source/table-proof.md new file mode 100644 index 0000000000..0a0b7d1fe4 --- /dev/null +++ b/research/nonreduced-tensor-recognition/source/table-proof.md @@ -0,0 +1,279 @@ +# Recognizing Frobenius-truncated algebras from a multiplication table + +This note gives a recognition criterion and an explicit algebra isomorphism +for a supplied multiplication table over a prime field. It answers a restricted +part of [Ali Romman's PR5459 handoff](https://github.com/CrocSwap/integer-mult-bounds/pull/5459): +the inputs and output already refer to one common vector space. It does not +recognize an arbitrary three-factor tensor up to independent basis changes, +extract its nucleus, or supply the reductions needed to obtain that table. +It does not change the selected multiplication bound. + +## 1. Input contract and criterion + +Let `p` be a prime and let `A` be a nonzero, `n`-dimensional vector space over +`F_p`, supplied by its structure constants. First check commutativity and +associativity on a basis. Recover the identity by solving the `n^2` linear +coordinate equations `u*b_j = b_j` for its `n` unknown coordinates, and check +the resulting element on both sides of every basis vector. Reject if the +system is inconsistent or any check fails. Commutativity makes a left unit +two-sided, and a two-sided unit is unique. These finite checks establish the +algebra laws for all inputs by bilinearity and trilinearity. A malformed table +is rejected before the criterion below. +Primality of `p` is a separate input condition: arithmetic modulo a composite +integer is not a substitute for `F_p`. + +Write `F(a) = a^p`. This is an `F_p`-linear endomorphism because `A` is +commutative of characteristic `p`. Let `E` be the least nonnegative integer +such that `p^E >= n`, and set + +``` +J = ker(F^E), Q = J / J^2, +K_e = ker(F^e), 0 <= e <= E, +V_e = (K_e + J^2) / J^2, 0 <= e <= E. +``` + +Here `F^0` is the identity, so `K_0 = 0`; `J^2` denotes the span of products +of pairs of elements of `J`. Every `K_e` lies in `J`, so `V_e` is a subspace +of `Q`. The spaces `V_e` form an increasing flag from `0` to `Q`. +Define + +``` +d_e = dim(V_e) - dim(V_(e-1)), 1 <= e <= E, +N = p^(sum_(e=1)^E e*d_e). +``` + +**Theorem.** The checked table is isomorphic, as a unital `F_p`-algebra, to + +``` +B = F_p[x_1,...,x_r] / (x_i^(p^h_i) : 1 <= i <= r), +``` + +with positive heights `h_i`, if and only if + +1. `J` has codimension one in `A`; and +2. `N = n`. + +The multiset of heights is intrinsic: height `e` occurs `d_e` times. An empty +list is allowed, in which case `B = F_p` and `n = 1`. Acceptance produces +generators `g_i` of the prescribed heights and the invertible matrix whose +columns are their ordered monomials. This matrix, together with its inverse, +is an explicit transporter between the common-basis multiplication tables. + +The word "transporter" here means an algebra isomorphism with the same map +on both inputs and the output. It is not a general tensor-orbit transporter. + +## 2. The Frobenius kernel recovers the nilradical + +Let `R` be the set of nilpotent elements of `A`. In a commutative algebra, +`R` is an ideal: products with arbitrary elements remain nilpotent, and a +sufficiently large characteristic-power Frobenius kills a sum of nilpotents. +Choose a vector-space basis `b_1,...,b_s` of `R` and positive integers `t_i` +with `b_i^t_i = 0`. The ideal generated by these basis elements is `R`. +Every product of more than `sum_i(t_i-1)` generators contains a factor +`b_i^t_i`, so `R` is nilpotent. + +The chain `R, R^2, ...` decreases strictly until it reaches zero. Indeed, if +`R^k = R^(k+1)` is nonzero, multiplication by further powers of `R` makes all +later powers equal, contrary to nilpotence. Since `1` is not nilpotent, +`dim R <= n-1`, and therefore `R^n = 0`. + +Every `a` in `R` consequently satisfies `a^(p^E) = 0`, since `p^E >= n`. +Conversely, every element killed by this power is nilpotent. Thus + +``` +J = ker(F^E) = R, J^n = 0. +``` + +This derivation does not assume that `A` is local. If `J` has codimension +one, the scalar copy of `F_p` maps isomorphically onto `A/J`. Every element +outside `J` then has the form `c*1 + j`, where `c != 0` and `j` is nilpotent, +and is a unit by a finite geometric series. Thus `J` is the unique maximal +ideal, and `A` is local with residue field exactly `F_p`. + +The codimension test matters. An extension field such as `F_4` viewed as a +two-dimensional `F_2`-algebra has zero nilradical but is outside this family; +its radical has codimension two. Products of fields are also excluded. + +## 3. Necessity and recovery of the heights + +Suppose `A` is the displayed algebra `B`. Its monomials + +``` +x_1^a_1 ... x_r^a_r, 0 <= a_i < p^h_i, +``` + +form a basis, its radical is `(x_1,...,x_r)`, and its tangent quotient +`J/J^2` has basis the classes of `x_1,...,x_r`. In particular, `J` has +codimension one and `n = p^(sum_i h_i)`, so every `h_i <= E`. + +If `h_i <= e`, the element `x_i` is killed by `F^e`; hence its tangent class +belongs to `V_e`. For the converse, expand an element of `J` into its linear +part and terms of total degree at least two. Frobenius raises every monomial +separately to its `p^e` power. If the linear coefficient of `x_i` is nonzero +and `h_i > e`, its power contributes a nonzero coefficient to +`x_i^(p^e)`. No other linear term contributes that monomial. No term of +degree at least two contributes it either: its surviving power has total +degree at least `2*p^e`; terms killed by a truncation contribute nothing. +Thus this coefficient cannot cancel. + +It follows that + +``` +V_e = span{x_i + J^2 : h_i <= e}. +``` + +Therefore `d_e` counts exactly the variables of height `e`, and the dimension +test `N = n` is necessary. The proof is invariant under an arbitrary common +change of basis in `A`; the identity and radical need not be coordinate axes +in the supplied table. + +## 4. Sufficiency and an explicit transporter + +Assume the two tests pass. For `e = 1,...,E`, extend a basis of `V_(e-1)` +to a basis of `V_e`, and choose the new representatives directly in `K_e`. +Linear elimination on a basis of `K_e` modulo `J^2` constructs such lifts. +Call the resulting elements `g_1,...,g_r` and assign height `h_i = e` to +each lift added at step `e`. Their classes form a basis of `J/J^2`, and + +``` +g_i^(p^h_i) = 0. +``` + +The classes are chosen by first entry into the flag. A lift of height `e` +cannot already lie in `K_(e-1)`, since its class would then belong to +`V_(e-1)`. No minimal-exponent test beyond the flag is needed. + +The substitution `x_i -> g_i` defines a unital homomorphism `phi: B -> A`, +because all defining power relations hold. It is surjective. One way to see +this is the finite nilpotent form of Nakayama's lemma. Let `C` be the +`F_p`-subalgebra generated by `1,g_1,...,g_r`. Tangent spanning gives +`J = span{g_i} + J^2`. For each `k >= 1`, express every factor of a product +in `J^k` by this formula. The product of its linear parts lies in `C`, and +every other term lies in `J^(k+1)`. Hence + +``` +J^k is contained in C + J^(k+1). +``` + +Starting from `A = F_p*1 + J` and repeating this containment through +`J^n = 0` gives `A = C`. This is also a direct proof of the particular +Nakayama implication used here. + +The monomial basis of `B` has size `N`, and the dimension test gives +`dim B = N = n = dim A`. A surjective linear map between these equal finite +dimensions is invertible. Therefore `phi` is the required isomorphism. + +For a concrete certificate, order the exponent tuples in a declared fixed +order and form the matrix + +``` +P[:,a] = g_1^a_1 ... g_r^a_r +``` + +in the supplied basis of `A`. Its constant column is the recovered unit. +The certificate consists of the heights, the generators, `P` and `P^-1`. +An independent checker can verify invertibility, the unit column, every +defining generator power and every basis-product identity + +``` +mu_A(P*u_a, P*u_b) = P*mu_B(u_a,u_b). +``` + +These `n^2` identities imply the identity for all pairs by bilinearity. +They check the actual transporter rather than trusting the recognition +path or the inferred heights. Equivalently, +`mu_B(u,v) = P^-1*mu_A(P*u,P*v)`. + +## 5. Why tangent Frobenius alone is insufficient + +The filtration uses images of kernels in the algebra before quotienting: +`V_e = (ker(F^e) + J^2)/J^2`. It must not be replaced by the kernel of the +induced Frobenius on `J/J^2`. Indeed, `a^p` lies in `J^p`, which is contained +in `J^2` for every `p >= 2`, so the latter induced map is always zero. + +For the explicit example `A = F_2[t]/(t^4)`, the radical is +`span{t,t^2,t^3}`, with square `span{t^2,t^3}`. Squaring gives + +``` +(a*t + b*t^2 + c*t^3)^2 = a*t^2. +``` + +Thus `ker F = span{t^2,t^3}`, `V_1 = 0`, and `V_2 = J/J^2`. +The correct height list is `(2)`, and the predicted dimension is four. +Frobenius on the tangent quotient alone would give height `(1)` and the +wrong predicted dimension two. + +The dimension test also cannot be dropped. For the square-zero algebra +`F_2 + J` with `dim J = 3` and `J^2 = 0`, all three tangent generators have +height one, but `dim A = 4` while the predicted truncated-polynomial +dimension is `2^3 = 8`. The generators give a surjection with additional +relations, not an isomorphism to the proposed model. + +## 6. Finite algorithm, cost and the one-dimensional edge + +All steps are finite linear algebra and arithmetic on the supplied table: + +1. Validate commutativity and associativity on basis vectors, recover the + unit by solving its linear equations, and check it on both sides. +2. Compute Frobenius on basis vectors by repeated squaring, its successive + kernels through `E`, and a basis of `J^2` from pairwise radical products. +3. Compute the flag dimensions and adapted lifts by Gaussian elimination; + reject unless the codimension and dimension tests pass. +4. Enumerate the `n` model monomials after acceptance, compute their images, + invert the resulting matrix, and check every basis-product identity. + +For fixed prime `p`, this is a deterministic polynomial algorithm in `n`. +There are at most `ceil(log_2 n)` Frobenius levels. Dense table multiplication +uses at most `O(n^3)` field operations, and the table-law and final +basis-product checks use polynomially many such operations (a conservative +bound for a direct implementation is `O(n^6 + n^4 log p)`). No search over +all algebra elements, all bases or all automorphisms is required. Computing +the exact candidate dimension is polynomial: its bit length is at most +`O(n*(log n + log p))`, and is `O(n log n)` for fixed `p`. The implementation +may compute that exact integer. Optionally, capped multiplication can stop +as soon as the candidate exceeds `n`; it is not needed for the complexity +guarantee. + +Conditional on a prime input, ordinary field arithmetic and powering are +also polynomial in `log p`; primality certification must be accounted for +separately in any variable-characteristic bit-complexity claim. In particular, +trial division is sufficient for bounded prime fixtures but does not by itself +establish a polynomial primality algorithm in the bit length of `p`. + +When `n = 1`, `E = 0`, `J = ker F^0 = 0`, the flag and height list are +empty, and the empty product gives `N = 1`. The recovered unit is nonzero and +forms a basis, so `P` is its single coordinate column. This covers `F_p` +without introducing a height-zero variable. The zero-dimensional zero ring +is outside the input contract. + +## 7. Scope, credit and remaining work + +The criterion establishes the promised algebra class from a checked common- +basis multiplication table over `F_p`, and constructs an isomorphism to its +standard monomial model. The written theorem covers every such dimension; +the package's finite fixtures corroborate it and are not a proof-kernel check. + +The immediate source is Ali Romman's +[PR5459 at `bef3575ddb19c44434a7e2460a353eac7c4d9387`](https://github.com/aliromman/integer-mult-bounds/blob/bef3575ddb19c44434a7e2460a353eac7c4d9387/research/frobenius-tensor-characters/README.md), +which supplies the unequal-height family, height flag and explicit +recognition/transporter handoff. His [PR5316](https://github.com/CrocSwap/integer-mult-bounds/pull/5316) +supplies the truncated-polynomial predecessor. Nick Petalas's +[PR5252](https://github.com/CrocSwap/integer-mult-bounds/pull/5252) supplies +the finite-etale comparison, nilpotent follow-up and tensor normalization +context; David Ganz and the retained atlas lineage retain their credit. +Nilradicals, tangent generators, [Nakayama's lemma](https://stacks.math.columbia.edu/tag/00DV) +and truncated-polynomial presentations are classical commutative algebra. +Section 4 gives the finite nilpotent argument used here directly. No general literature +priority is claimed for this criterion. + +Open next questions are the reduction from an independently based input +tensor to this checked table, with certified factor maps, and an extension +to supplied non-prime finite fields with the appropriate semilinear +Frobenius calculations. Neither follows merely by applying the present +`F_p`-linear kernel routine. Determinant-character extraction can subsequently +use the verified heights under its separately pinned theorem. Physical +coefficient preparation, native realization, precision, movement, supplier +admission and a new multiplication exponent remain outside this result. + +Prepared by apoursh with substantial OpenAI Codex assistance. Apache-2.0. +No independent human or formal proof-kernel acceptance is claimed. diff --git a/research/nonreduced-tensor-recognition/source/table_source.py b/research/nonreduced-tensor-recognition/source/table_source.py new file mode 100644 index 0000000000..875be1c29d --- /dev/null +++ b/research/nonreduced-tensor-recognition/source/table_source.py @@ -0,0 +1,343 @@ +#!/usr/bin/env python3 +"""Exact recognition of prime-field Frobenius-truncated algebras. + +The input structure constants have convention table[i][j][k] = coefficient +of basis vector k in basis_i * basis_j. The basis need not contain the unit. +Integer coefficients are reduced modulo p. Malformed input raises ValueError; +a well-formed table outside the target class returns status="rejected". + +An accepted result includes an explicit change of basis, with transporter +stored as a list of columns and inverse stored as matrix rows. Exponent tuples +are in lexicographic order, with their last coordinate varying fastest. +validate_witness checks that certificate without running the recognition flag. + +Only Python's standard library is used. Primality uses trial division: the +fixed-prime recognition algorithm is polynomial in the algebra dimension, +but this primality check is not polynomial in the bit length of a variable p. +""" + +from itertools import product +from math import isqrt + + +def _integer(value): + return isinstance(value, int) and not isinstance(value, bool) + + +def _input(p, table): + if not _integer(p) or p < 2: + raise ValueError("p must be a prime integer") + if p != 2 and (p % 2 == 0 or any(p % d == 0 for d in range(3, isqrt(p) + 1, 2))): + raise ValueError("p must be prime") + if not isinstance(table, (list, tuple)) or not table: + raise ValueError("table must have positive dimension") + n = len(table) + normalized = [] + for plane in table: + if not isinstance(plane, (list, tuple)) or len(plane) != n: + raise ValueError("table must have shape n by n by n") + result_plane = [] + for row in plane: + if not isinstance(row, (list, tuple)) or len(row) != n: + raise ValueError("table must have shape n by n by n") + if any(not _integer(value) for value in row): + raise ValueError("structure constants must be integers") + result_plane.append([value % p for value in row]) + normalized.append(result_plane) + return normalized + + +def _basis(n): + return [[int(i == j) for i in range(n)] for j in range(n)] + + +def _multiply(left, right, table, p): + result = [0] * len(table) + for i, a in enumerate(left): + if a: + for j, b in enumerate(right): + if b: + scale = a * b + for k, value in enumerate(table[i][j]): + result[k] = (result[k] + scale * value) % p + return result + + +def _power(value, exponent, unit, table, p): + result = list(unit) + base = list(value) + while exponent: + if exponent & 1: + result = _multiply(result, base, table, p) + exponent //= 2 + if exponent: + base = _multiply(base, base, table, p) + return result + + +def _rref(matrix, p, coefficient_columns): + rows = [[value % p for value in row] for row in matrix] + pivots = [] + for column in range(coefficient_columns): + pivot = next((r for r in range(len(pivots), len(rows)) if rows[r][column]), None) + if pivot is None: + continue + row_index = len(pivots) + rows[row_index], rows[pivot] = rows[pivot], rows[row_index] + scale = pow(rows[row_index][column], -1, p) + rows[row_index] = [(scale * value) % p for value in rows[row_index]] + for r, row in enumerate(rows): + if r != row_index and row[column]: + scale = row[column] + rows[r] = [(a - scale * b) % p for a, b in zip(row, rows[row_index])] + pivots.append(column) + if len(pivots) == len(rows): + break + return rows, pivots + + +def _kernel(columns, p): + n = len(columns) + rows, pivots = _rref(list(map(list, zip(*columns))), p, n) + result = [] + for free in range(n): + if free not in pivots: + vector = [0] * n + vector[free] = 1 + for r, pivot in enumerate(pivots): + vector[pivot] = -rows[r][free] % p + result.append(vector) + return result + + +def _add_column(echelon, vector, p): + """Extend a column span in place; report whether its dimension increased.""" + reduced = [value % p for value in vector] + for pivot in sorted(echelon): + if reduced[pivot]: + scale = reduced[pivot] + reduced = [(a - scale * b) % p for a, b in zip(reduced, echelon[pivot])] + pivot = next((i for i, value in enumerate(reduced) if value), None) + if pivot is None: + return False + scale = pow(reduced[pivot], -1, p) + echelon[pivot] = [(scale * value) % p for value in reduced] + return True + + +def _apply(columns, vector, p): + return [sum(columns[j][i] * vector[j] for j in range(len(columns))) % p + for i in range(len(columns))] + + +def _inverse(columns, p): + n = len(columns) + rows = [[columns[j][i] for j in range(n)] + [int(i == j) for j in range(n)] + for i in range(n)] + reduced, pivots = _rref(rows, p, n) + if len(pivots) != n: + return None + return [row[n:] for row in reduced] + + +def _algebra_error(table, p): + n = len(table) + for i in range(n): + for j in range(i): + if table[i][j] != table[j][i]: + return "table is not commutative" + for i in range(n): + for j in range(n): + for k in range(n): + left = [sum(table[i][j][r] * table[r][k][s] for r in range(n)) % p + for s in range(n)] + right = [sum(table[j][k][r] * table[i][r][s] for r in range(n)) % p + for s in range(n)] + if left != right: + return "table is not associative" + return None + + +def _recover_unit(table, p): + n = len(table) + equations = [[table[i][j][k] for i in range(n)] + [int(j == k)] + for j in range(n) for k in range(n)] + reduced, pivots = _rref(equations, p, n) + if any(not any(row[:n]) and row[n] for row in reduced): + return None + unit = [0] * n + for r, pivot in enumerate(pivots): + unit[pivot] = reduced[r][n] + for vector in _basis(n): + if _multiply(unit, vector, table, p) != vector: + return None + if _multiply(vector, unit, table, p) != vector: + return None + return unit + + +def recognize(p, table): + """Recognize a tensor product of F_p[x]/(x**(p**h)), and return its witness.""" + table = _input(p, table) + n = len(table) + result = {"status": "rejected", "reason": "", "n": n} + error = _algebra_error(table, p) + if error is not None: + result["reason"] = error + return result + unit = _recover_unit(table, p) + if unit is None: + result["reason"] = "table has no two-sided unit" + return result + + levels, bound = 0, 1 + while bound < n: + levels += 1 + bound *= p + result["frobenius_levels"] = levels + identity = _basis(n) + kernels = [] + current = identity + if levels: + frobenius = [_power(vector, p, unit, table, p) for vector in identity] + for _ in range(levels): + current = [_apply(frobenius, column, p) for column in current] + kernels.append(_kernel(current, p)) + radical = kernels[-1] if kernels else [] + result["radical_dimension"] = len(radical) + if len(radical) != n - 1: + result["reason"] = "Frobenius radical does not have codimension one" + return result + + quotient_span = {} + for i, left in enumerate(radical): + for right in radical[i:]: + _add_column(quotient_span, _multiply(left, right, table, p), p) + square_dimension = len(quotient_span) + result["radical_square_dimension"] = square_dimension + result["tangent_dimension"] = len(radical) - square_dimension + heights, generators, flag_dimensions = [], [], [0] + for e, kernel in enumerate(kernels, 1): + for vector in kernel: + if _add_column(quotient_span, vector, p): + heights.append(e) + generators.append(list(vector)) + flag_dimensions.append(len(quotient_span) - square_dimension) + result["flag_dimensions"] = flag_dimensions + result["heights"] = heights + predicted_dimension = p ** sum(heights) + result["predicted_dimension"] = predicted_dimension + if predicted_dimension != n: + result["reason"] = "adapted Frobenius heights predict a different dimension" + return result + + sizes = [p ** height for height in heights] + exponents = [list(exponent) for exponent in product(*(range(size) for size in sizes))] + powers = [[_power(generator, exponent, unit, table, p) for exponent in range(size)] + for generator, size in zip(generators, sizes)] + columns = [] + for exponent in exponents: + column = list(unit) + for i, value in enumerate(exponent): + column = _multiply(column, powers[i][value], table, p) + columns.append(column) + inverse = _inverse(columns, p) + if inverse is None: + raise RuntimeError("recognition criterion passed but monomial columns are singular") + result.update({ + "status": "accepted", + "reason": "verified monomial basis for the recovered Frobenius heights", + "generators": generators, + "unit": unit, + "monomial_exponents": exponents, + "transporter": columns, + "inverse": inverse, + }) + validate_witness(p, table, result) + return result + + +def _witness_vectors(value, count, n, p, name): + if not isinstance(value, list) or len(value) != count: + raise ValueError(name + " has the wrong number of vectors") + for vector in value: + if not isinstance(vector, list) or len(vector) != n: + raise ValueError(name + " has a vector of the wrong dimension") + if any(not _integer(a) or not 0 <= a < p for a in vector): + raise ValueError(name + " must use canonical prime-field coordinates") + return value + + +def validate_witness(p, table, witness): + """Independently check a monomial transporter; return True or raise ValueError. + + The checker does not use Frobenius kernels, radical dimensions, flag + dimensions or the recognition decision. It verifies the declared model's + relations, all monomial columns, both inverse identities, and every basis + product in the source and target. Diagnostic fields are not certificate + premises and are deliberately ignored. + """ + table = _input(p, table) + n = len(table) + if not isinstance(witness, dict) or witness.get("status") != "accepted": + raise ValueError("witness must declare an accepted certificate") + if not _integer(witness.get("n")) or witness["n"] != n: + raise ValueError("witness dimension does not match table") + error = _algebra_error(table, p) + if error is not None: + raise ValueError(error) + unit = _witness_vectors([witness.get("unit")], 1, n, p, "unit")[0] + for vector in _basis(n): + if _multiply(unit, vector, table, p) != vector or _multiply(vector, unit, table, p) != vector: + raise ValueError("witness unit does not satisfy both identity laws") + heights = witness.get("heights") + if not isinstance(heights, list) or any(not _integer(h) or h < 1 for h in heights): + raise ValueError("heights must be positive integers") + # This bound also avoids constructing a maliciously large p**h value. + maximum_height, bound = 0, 1 + while bound < n: + maximum_height += 1 + bound *= p + if any(h > maximum_height for h in heights) or len(heights) > n: + raise ValueError("heights exceed the model dimension") + if p ** sum(heights) != n: + raise ValueError("model dimension does not match table") + generators = _witness_vectors(witness.get("generators"), len(heights), n, p, "generators") + columns = _witness_vectors(witness.get("transporter"), n, n, p, "transporter") + inverse = _witness_vectors(witness.get("inverse"), n, n, p, "inverse") + sizes = [p ** height for height in heights] + exponents = [list(exponent) for exponent in product(*(range(size) for size in sizes))] + supplied_exponents = witness.get("monomial_exponents") + if not isinstance(supplied_exponents, list) or len(supplied_exponents) != n: + raise ValueError("monomial exponents have the wrong length") + for actual, expected in zip(supplied_exponents, exponents): + if not isinstance(actual, list) or any(not _integer(a) for a in actual) or actual != expected: + raise ValueError("monomial exponents are not the declared lexicographic order") + zero = [0] * n + powers = [] + for generator, height, size in zip(generators, heights, sizes): + if _power(generator, size, unit, table, p) != zero: + raise ValueError("generator violates its truncation relation") + if _power(generator, p ** (height - 1), unit, table, p) == zero: + raise ValueError("generator has a smaller Frobenius height") + powers.append([_power(generator, a, unit, table, p) for a in range(size)]) + for column, exponent in zip(columns, exponents): + expected = list(unit) + for i, a in enumerate(exponent): + expected = _multiply(expected, powers[i][a], table, p) + if column != expected: + raise ValueError("transporter column does not equal its generator monomial") + for i in range(n): + for j in range(n): + if sum(columns[k][i] * inverse[k][j] for k in range(n)) % p != int(i == j): + raise ValueError("transporter times inverse is not the identity") + if sum(inverse[i][k] * columns[j][k] for k in range(n)) % p != int(i == j): + raise ValueError("inverse times transporter is not the identity") + monomial_index = {tuple(exponent): i for i, exponent in enumerate(exponents)} + for i, a in enumerate(exponents): + for j, b in enumerate(exponents): + combined = tuple(x + y for x, y in zip(a, b)) + expected = zero if any(x >= size for x, size in zip(combined, sizes)) else columns[monomial_index[combined]] + if _multiply(columns[i], columns[j], table, p) != expected: + raise ValueError("transporter fails a model basis-product identity") + return True diff --git a/research/nonreduced-tensor-recognition/verify.py b/research/nonreduced-tensor-recognition/verify.py new file mode 100644 index 0000000000..155a6043c1 --- /dev/null +++ b/research/nonreduced-tensor-recognition/verify.py @@ -0,0 +1,223 @@ +"""Check recognition against complete tiny orbits and literal products. + +Ali Romman with substantial OpenAI Codex assistance. Apache-2.0. +""" +from copy import deepcopy +import hashlib +import importlib.util +from itertools import product +import json +from pathlib import Path + +HERE = Path(__file__).resolve().parent +spec = importlib.util.spec_from_file_location('recognition', HERE / 'recognize.py') +R = importlib.util.module_from_spec(spec) +spec.loader.exec_module(R) +N = R.N + + +def require(test, message): + if not test: + raise ValueError(message) + + +def action(t, u, v, p): + out = [0] * len(t) + for i, x in enumerate(u): + for j, y in enumerate(v): + for k, z in enumerate(t[i][j]): + out[k] = (out[k] + x*y*z) % p + return out + + +def model(p, heights): + sizes = [p**h for h in heights] + exponents = list(product(*(range(s) for s in sizes))) + index = {e:i for i,e in enumerate(exponents)} + n = len(exponents) + t = [[[0]*n for _ in range(n)] for _ in range(n)] + for i,a in enumerate(exponents): + for j,b in enumerate(exponents): + c = tuple(x+y for x,y in zip(a,b)) + if all(x1: + for step in range(2*n+trial): + i=(step+salt)%n; j=(i+1)%n + a[i]=[(x+(trial+salt+1)*y)%p for x,y in zip(a[i],a[j])] + a=a[1:]+a[:1] + else: + a[0][0]=1+(trial+salt)%(p-1) + maps.append(a) + yield change(t,*maps,p) + + +def main(): + controls=0 + def rejected(call): + nonlocal controls + try: + call() + except ValueError: + controls+=1 + else: + raise ValueError('negative control passed') + for pin in json.loads((HERE/'INPUTS.json').read_text()): + data=(HERE/pin['local']).read_bytes() + blob=b'blob '+str(len(data)).encode()+b'\0'+data + require(hashlib.sha256(data).hexdigest()==pin['sha256'] and + hashlib.sha1(blob).hexdigest()==pin['git_blob'], 'source copy') + rejected(lambda: require(hashlib.sha256(data+b' ').hexdigest()==pin['sha256'], 'source corruption')) + domains,products=census() + fixtures=0 + for p,heights in [(2,[]),(2,[1]),(2,[2]),(2,[1,1]),(2,[1,2]),(3,[1]),(3,[2]),(5,[1]),(7,[1]),(13,[])]: + t=model(p,heights) + for changed in presentations(t,p,2 if len(t)>7 else 3): + cert,reason=R.recognize_frobenius(changed,p) + require(cert is not None and cert['heights']==heights, 'Frobenius presentation') + products+=check(changed,cert,p,t); fixtures+=1 + square_zero=[[[0]*3 for _ in range(3)] for _ in range(3)] + for i in range(3):square_zero[0][i][i]=square_zero[i][0][i]=1 + general=[(2,square_zero),(2,quotient(2,[1,0,1,0,1])), + (3,direct_sum(model(3,[1]),model(3,[]))), + (2,direct_sum(square_zero,model(2,[])))] + for p,t in general: + for changed in presentations(t,p): + cert,reason=R.recognize_commutative(changed,p) + require(cert is not None, 'general commutative algebra rejected') + products+=check(changed,cert,p); fixtures+=1 + specific,reason=R.recognize_frobenius(changed,p) + require(specific is None and reason.startswith('algebra class:'), 'class boundary') + controls+=1 + for p in [2,3,5]: + dual=[[[square_zero[i][k][j] for k in range(3)] for j in range(3)] for i in range(3)] + for transpose_input,reason in [(False,'output top multiplicity'),(True,'input top multiplicity')]: + t=[[dual[j][i] for j in range(3)] for i in range(3)] if transpose_input else dual + X,Y=N.nucleus(t,p) + require(len(X)==3 and all(N.rank([N.flat(a) for a in b],p)==3 for b in [X,Y]), 'faithful dual control') + cert,why=R.recognize_commutative(t,p) + require(cert is None and why==reason, 'nonregular module accepted'); controls+=1 + a=[[[0]*4 for _ in range(4)] for _ in range(4)] + for i,j,k,l in product(range(2),repeat=4): + if j==k:a[2*i+j][2*k+l][2*i+l]=1 + cert,why=R.recognize_commutative(a,2) + require(cert is None and why=='noncommutative nucleus','noncommutative algebra'); controls+=1 + t=model(2,[2]); cert,_=R.recognize_frobenius(t,2) + for name in ['U','V','W']: + for i,j in product(range(4),repeat=2): + changed=deepcopy(cert); changed[name][i][j]^=1 + rejected(lambda:check(t,changed,2,t)) + for t,p in [([],2),([[[0]]],4),([[[True]]],2),([[[2]]],2),([[[0,0]]],2),([[[1]]],True)]: + rejected(lambda:R.recognize_commutative(t,p)) + print('PASS_NONREDUCED_TENSOR_RECOGNITION '+json.dumps(dict(binary=domains, + presentations=fixtures,literal_basis_products=products,controls=controls),sort_keys=True)) + + +if __name__=='__main__': + main()