Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 16 additions & 4 deletions family_equiv.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,11 +169,23 @@ def stage2(Msrc, Mtgt, sname, tname):
return None


def parse_roots(s):
"""Parse 'r1,r2,r3' with rational entries into a root list."""
return [0] + [Q(sp.Rational(t)) for t in s.split(',')]


def main():
MA = member_kit([0, -1, 3, 4])
MB = member_kit([0, -1, -2, Q(3, 2)])
print("--- affine-equivalence decision, degrees strictly separated ---",
flush=True)
import argparse
ap = argparse.ArgumentParser()
ap.add_argument('--pair', default='-1,3,4;-2,-1,3/2',
help="two members as 'a,b,c;d,e,f' (nonzero roots; "
"0 is implicit). Default: the original A, B.")
args = ap.parse_args()
ra, rb = args.pair.split(';')
MA = member_kit(parse_roots(ra))
MB = member_kit(parse_roots(rb))
print("--- affine-equivalence decision for {0,%s} vs {0,%s} ---"
% (ra, rb), flush=True)
for name, M in (('A', MA), ('B', MB)):
degs = [sp.total_degree(sp.expand(Fi), x, y, z) for Fi in M['F']]
assert degs == [12, 11, 4], degs
Expand Down
26 changes: 17 additions & 9 deletions family_hunt.py
Original file line number Diff line number Diff line change
Expand Up @@ -313,14 +313,22 @@ def n1_exact(M, target):
RTOL_REJECT = mp.mpf('1e-20')


def _mp_kit(M):
"""Per-member mpf constants for phase 2 (cached on the kit)."""
if '_mp' not in M:
tompf = lambda q: mp.mpf(int(q.p)) / int(q.q)
M['_mp'] = dict(
cH=[tompf(c) for c in sp.Poly(M['H'], w).all_coeffs()],
cp=[tompf(c) for c in sp.Poly(M['p'], w).all_coeffs()],
Hp0=tompf(M['Hp0']), a=tompf(M['a']))
def _mp_kit(M, dps=60):
"""Per-member mpf constants for phase 2, built INSIDE an explicit
precision context and cached with their precision. (Building them
at ambient precision was a latent bug: a direct n2_numeric caller
in a fresh process got 15-digit constants under 60-digit
thresholds, flipping marginal targets; the published runs were
unaffected only because the calibration gate happened to build the
cache inside workdps(60) first.)"""
if M.get('_mp_dps', 0) < dps:
with mp.workdps(dps):
tompf = lambda q: mp.mpf(int(q.p)) / int(q.q)
M['_mp'] = dict(
cH=[tompf(c) for c in sp.Poly(M['H'], w).all_coeffs()],
cp=[tompf(c) for c in sp.Poly(M['p'], w).all_coeffs()],
Hp0=tompf(M['Hp0']), a=tompf(M['a']))
M['_mp_dps'] = dps
return M['_mp']


Expand Down Expand Up @@ -368,7 +376,7 @@ def n2_numeric(M, target, dps=60):
Av, Bv, Cv = target
if Cv == 0:
return None
K = _mp_kit(M)
K = _mp_kit(M, dps)
with mp.workdps(dps):
tompf = lambda q: mp.mpf(int(q.p)) / int(q.q)
Av, Bv, Cv = tompf(Q(Av)), tompf(Q(Bv)), tompf(Q(Cv))
Expand Down
137 changes: 137 additions & 0 deletions family_screen.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
#!/usr/bin/env python3
"""Screen the degree-4 endpoint-identity moduli for G0-G2-tied pairs.

The completeness hunt (issue #14) needs a pair of members whose
multiplicity landscapes tie through G2 (the census pipeline separated
the original pair (A, B) at G2 -- PR #20). This script enumerates
small-height rational members and screens them pairwise with the same
validated instrument (family_gluing_census.py).

Moduli parametrization: roots {0, a, b, c} satisfy the endpoint
identity prod(1 - r_i) = prod_{i>=2}(-r_i) iff e2 = e1 - 1, i.e.

c = (1-a)(1-b) / [ (1-a)(1-b) - a*b ] (rational in a, b).

Screening outputs are MEASUREMENTS with the CI-pinned instrument, not
banked adoptions: any tied pair found here still needs its own gate run
(V1 grid on the new members) and a non-triviality decision
(family_equiv.py-style, plus the tame caveat) before it becomes the
completeness test case. Members that violate an engine assumption
(phantom arcs, degenerate origin structure, ...) raise loudly and are
recorded as SKIPPED with the reason -- never silently dropped.
"""
import argparse
import sys
import time

import sympy as sp
from sympy import Rational as Q

from family_hunt import member_kit
from family_gluing_census import (CensusError, PlaneCensus, plane_data,
point_oracle_factory, assemble_3d,
Ps, Rs)


def enumerate_members(vals):
"""Distinct valid members {0, a, b, c} from a list of rational
candidate values for (a, b)."""
seen = {}
order = []
for a in vals:
for b in vals:
if a == b or 0 in (a, b) or 1 in (a, b):
continue
den = (1 - a)*(1 - b) - a*b
if den == 0:
continue
c = (1 - a)*(1 - b)/den
if c in (0, 1, a, b):
continue
key = tuple(sorted([a, b, Q(c)]))
if key in seen:
continue
seen[key] = True
order.append(key)
return order


def screen_member(roots_key):
"""Build + census one member; returns dict or raises."""
M = member_kit([0] + list(roots_key))
EPR, D, label = plane_data(M)
# allow_folds=True: a smooth vertical tangent in the (P,R) frame is
# a presentation feature, not an intrinsic stratum; the engine
# coarsens it away correctly (L5 made folds *absent* for A and B,
# but absence is not required for correctness)
pc = PlaneCensus(D, Ps, Rs, label, point_oracle_factory(4, True))
cx = pc.result()
g3d = assemble_3d(M, pc, cx)
return dict(M=M, plane=cx, g3d=g3d,
census=tuple(sorted(g3d.census().items())))


def main():
ap = argparse.ArgumentParser()
ap.add_argument('--halves', action='store_true',
help='include half-integer parameter values')
ap.add_argument('--span', type=int, default=5,
help='integer parameter range [-span, span]')
args = ap.parse_args()

vals = [Q(k) for k in range(-args.span, args.span + 1)]
if args.halves:
vals += [Q(2*k + 1, 2) for k in range(-args.span, args.span)]
members = enumerate_members(vals)
print("moduli candidates (distinct root triples): %d" % len(members))

results = {}
skipped = []
t00 = time.time()
for i, key in enumerate(members):
t0 = time.time()
try:
results[key] = screen_member(key)
print("[%d/%d] %s: census %s [%ds]"
% (i + 1, len(members), key,
dict(results[key]['census']), time.time() - t0),
flush=True)
except (CensusError, ValueError, AssertionError) as e:
skipped.append((key, str(e)))
print("[%d/%d] %s: SKIPPED (%s) [%ds]"
% (i + 1, len(members), key, e, time.time() - t0),
flush=True)
sp.core.cache.clear_cache()
print("censused %d members, skipped %d, total %dmin"
% (len(results), len(skipped), (time.time() - t00)/60))

# group by G1 census (G0 is contained in it)
groups = {}
for key, r in results.items():
groups.setdefault(r['census'], []).append(key)
print("\nG1 census groups:")
tied_pairs = []
for cen, keys in sorted(groups.items(), key=lambda kv: -len(kv[1])):
print(" census %s: %d members" % (dict(cen), len(keys)))
for k in keys:
print(" %s" % (k,))
# pairwise G2 within the group
for i in range(len(keys)):
for j in range(i + 1, len(keys)):
a, b = keys[i], keys[j]
if results[a]['g3d'].iso(results[b]['g3d']):
tied_pairs.append((a, b))
print(" G2 TIE: %s ~ %s" % (a, b))

print("\n%d G0-G2-tied pair(s) found" % len(tied_pairs))
for a, b in tied_pairs:
print(" %s ~ %s" % (a, b))
if skipped:
print("\nskipped members (engine assumption violated):")
for k, e in skipped:
print(" %s: %s" % (k, e))
return 0


if __name__ == '__main__':
sys.exit(main())
Loading