From 892ba5bbfb4ff742116df89c2bc89b02ec63c654 Mon Sep 17 00:00:00 2001 From: Sebastian Lindner <33971232+salindne@users.noreply.github.com> Date: Wed, 26 Aug 2026 13:13:00 -0600 Subject: [PATCH 1/4] split g3 arb addition: carry the full adjugate, +1M -12A on the generic path The T-block already cost 15M 0S 9A for three adjugate entries. Computing the bottom row from columns 1 and 2 of T and shifting it costs the SAME 15M 0S 9A and yields seven, because column 3 of T is x*(column 2) reduced mod up, so the six remaining entries are one multiplication each rather than a 2x2 minor. The determinant then reads m8 rather than m3, since t7*m3 = (-up0*t7)*m8 = t2*m8. So the block conversion is free and the saving is downstream. With the full adjugate in hand the generic path applies it as a matrix-vector product instead of forming q = m7x^2 + m4x + m1 and reducing vt*q mod up by Karatsuba twice: 12M 11A against 11M 23A. Deg3ADD generic 65M 3S 87A 12C -> 66M 3S 75A 12C. No other shape moves. The 1M:3A trade rule accepts +1M -12A comfortably. Established before the edit rather than after: the six shift identities and both determinant forms verified over 4000 trials on GF(10007); t9 shown never to appear below the prefix, t3 and t6 shown to be reused temps whose every read is preceded by its own local assignment, and every Karatsuba temp t0-t5 reassigned before being read in the rest of the generic path. adjugate.py keeps the pre-C4 route as split_q_col1, which measures the same 15M 0S 9A for four entries against the new block's seven, and its .mag anchor moves to the new determinant line. selftest.py's published pin for this cell is superseded with the reason recorded: the thesis is not wrong, the formula moved under it, and Thesis/ERRATA.md carries the divergence. Gates: hand count agreeing at +1M -12A independently of opcount, whitebox 7043/7043, driver --strict 55236/55236, dominance clean on 39 files, adjugate ok, selftest 19/0/0, Magma 30 testers 0 failures 0 skips in 5m13s. --- .../negReduced/g3Formulas/arb_splitG3_ADD.mag | 45 ++++++++++++------- verification/adjugate.py | 41 ++++++++++++++--- verification/selftest.py | 8 +++- 3 files changed, 72 insertions(+), 22 deletions(-) diff --git a/g3/splitModel/negReduced/g3Formulas/arb_splitG3_ADD.mag b/g3/splitModel/negReduced/g3Formulas/arb_splitG3_ADD.mag index 1bbd95e..5ea92fc 100644 --- a/g3/splitModel/negReduced/g3Formulas/arb_splitG3_ADD.mag +++ b/g3/splitModel/negReduced/g3Formulas/arb_splitG3_ADD.mag @@ -8391,14 +8391,26 @@ Deg3ADD:= function(u2,u1,u0,v2,v1,v0,up2,up1,up0,vp2,vp1,vp0,ccs) t2 := -up0*t7; t5 := t1 - up1*t7; t8 := t4 - up2*t7; - t3 := -up0*t8; - t6 := t2 - up1*t8; - t9 := t5 - up2*t8; + //t3 := -up0*t8; + //t6 := t2 - up1*t8; + //t9 := t5 - up2*t8; - m1 := t5*t9 - t6*t8; - m4 := t6*t7 - t4*t9; + // Bottom row of the adjugate needs only columns 1 and 2 of T. + m9 := t1*t5 - t2*t4; + m8 := t2*t7 - t1*t8; m7 := t4*t8 - t5*t7; - d := t1*m1 + t2*m4 + t3*m7; //Determinant of T = t1*m1 + t2*m4 + t3*m7 + + // Column 3 of T is x*(column 2) reduced mod up, so the remaining six + // adjugate entries are shifts of the bottom row with one multiplication + // each rather than a 2x2 minor. m3, m4 and m6 are the three the + // determinant does not read, so they wait until the generic path below. + m5 := m9 + up2*m8; + m2 := -up0*m7; + m1 := m5 + up1*m7; + + // t7*m3 = t7*(-up0*m8) = (-up0*t7)*m8 = t2*m8, the signs cancelling because + // t2 and m3 carry the same -up0 factor. So the determinant needs m8, not m3. + d := t1*m1 + t4*m2 + t2*m8; //Determinant of T if IsZero(d) then //dw1 := u mod up; // = (a1^-1)*S for S,a1,b1 = XGCD(u,up); @@ -10042,16 +10054,17 @@ Deg3ADD:= function(u2,u1,u0,v2,v1,v0,up2,up1,up0,vp2,vp1,vp0,ccs) vt1:= vp1 - v1; vt0:= vp0 - v0; - //Compute s = vt*q mod u2 using Karatsuba twice - t0 := vt0*m1; - t1 := vt1*m4; - t2 := vt2*m7; - t3 := (m4 + m7)*(vt1 + vt2) - t2 - t1 - up2*t2; - t4 := up1*t2; - t5 := t4 - t1; - sp0 := t0 - up0*t3; - sp1 := (m1 + m4)*(vt0 + vt1) - sp0 - (up0 + up1)*(t2 + t3) + t5; - sp2 := (m1 + m7)*(vt0 + vt2) - t0 - t2 - t5 - up2*t3; + // the three adjugate entries the determinant did not need + m4 := m8 + up2*m7; + m6 := m2 - up1*m8; + m3 := -up0*m8; + + //Compute s = vt*q mod up as the matrix-vector product sp = M*vt. M is + //multiplication by q = d/u mod up, so this is the same map as vt*q mod up, + //and applying the matrix needs no reduction step. + sp0 := vt0*m1 + vt1*m2 + vt2*m3; + sp1 := vt0*m4 + vt1*m5 + vt2*m6; + sp2 := vt0*m7 + vt1*m8 + vt2*m9; if IsZero(sp2) then if IsZero(sp1) then diff --git a/verification/adjugate.py b/verification/adjugate.py index aefb89d..ab85d0b 100644 --- a/verification/adjugate.py +++ b/verification/adjugate.py @@ -1032,13 +1032,17 @@ def shipped_9(t1, t4, t7, up0, up1, up2): m9=m9, d=d) -def split_q(t1, t4, t7, up0, up1, up2): - """arb_splitG3_ADD.mag, `t1 := u0 - up0;` .. `d := t1*m1 + t2*m4 + t3*m7;`. +def split_q_col1(t1, t4, t7, up0, up1, up2): + """The split route as shipped BEFORE C4: `d := t1*m1 + t2*m4 + t3*m7;`. The whole first column, and d. - The split model wants only column 1 of the adjugate (it is the coefficient - vector of q = d/w mod up), and pays for all nine T entries to get it. + It wants only column 1 of the adjugate (the coefficient vector of + q = d/w mod up) and pays for all nine T entries to get it. Kept as a + candidate rather than deleted: it is a valid route and the comparison should + keep scoring it, and it is the thing C4 measured itself against. Note it + costs the SAME 15M 0S 9A as the shift route below -- the block conversion + alone wins nothing, which is why C4's unit of work is the generic path. """ t2 = -(up0 * t7) t5 = t1 - up1 * t7 @@ -1053,6 +1057,32 @@ def split_q(t1, t4, t7, up0, up1, up2): return dict(m1=m1, m4=m4, m7=m7, d=d) +def split_q(t1, t4, t7, up0, up1, up2): + """arb_splitG3_ADD.mag, `t2 := -up0*t7;` .. `d := t1*m1 + t4*m2 + t2*m8;`. + + The split route as shipped SINCE C4, and textually the ramified block with + `up` for `u`: bottom row of the adjugate from columns 1 and 2 of T, then six + shifts at one multiplication each, then the column expansion that reads m8 + rather than m3 because t7*m3 = t2*m8. + + Column 3 of T is x*(column 2) reduced mod up, which is what makes the six + remaining entries shifts rather than 2x2 minors. m3, m4 and m6 are the three + the determinant does not read, so the file defers them to the generic path + and no degenerate leaf pays for them. + """ + t2 = -(up0 * t7) + t5 = t1 - up1 * t7 + t8 = t4 - up2 * t7 + m9 = t1 * t5 - t2 * t4 + m8 = t2 * t7 - t1 * t8 + m7 = t4 * t8 - t5 * t7 + m5 = m9 + up2 * m8 + m2 = -(up0 * m7) + m1 = m5 + up1 * m7 + d = t1 * m1 + t4 * m2 + t2 * m8 + return dict(m1=m1, m2=m2, m5=m5, m7=m7, m8=m8, m9=m9, d=d) + + def _rank5_row3(t1, t4, t7, up0, up1, up2): """The five-product bottom row, and column 2 of T. Shared by the variants. @@ -1175,6 +1205,7 @@ def __init__(self, name, fn, nargs, expect=None, annot=None, group="entries"): ("arb_ramifiedG3_DBL.mag", "block")), Candidate("shipped_9", shipped_9, 6, (18, 0, 11)), Candidate("split_q", split_q, 6, (15, 0, 9)), + Candidate("split_q_col1", split_q_col1, 6, (15, 0, 9)), Candidate("rank5_7_d", rank5_7_d, 6, (15, 0, 14)), Candidate("rank5_7", rank5_7, 6, (12, 0, 12)), Candidate("rank5_9_d", rank5_9_d, 6, (17, 0, 16)), @@ -1932,7 +1963,7 @@ def section_bound(primes=(2, 3, 5, 7, 11), verbose=False): "t2 := -u0*t7;", "d := t1*m1 + t4*m2 + t2*m8;", {"u0": "up0", "u1": "up1", "u2": "up2"}), ("split_q", "g3/splitModel/negReduced/g3Formulas/arb_splitG3_ADD.mag", - "t2 := -up0*t7;", "d := t1*m1 + t2*m4 + t3*m7;", {}), + "t2 := -up0*t7;", "d := t1*m1 + t4*m2 + t2*m8;", {}), ) # The three `t1 = u0 - up0` differences are deliberately outside every anchor diff --git a/verification/selftest.py b/verification/selftest.py index 11a12e5..3f866f9 100644 --- a/verification/selftest.py +++ b/verification/selftest.py @@ -1874,7 +1874,13 @@ def section_split_counts(rep, quick): # every tuple a published cell and every delta measured. PINS = [ ("splitneg/g3/arb", 31, { - "33ADD n=0,0": ((65, 3, 87, 12, 1), 2), # Degree 3 + # SUPERSEDED BY C4, not a disagreement with the thesis. The published + # cell is (65, 3, 87, 12, 1); the formula changed under it, trading one + # multiplication for twelve additions by carrying the full adjugate and + # applying it as a matrix-vector product. Hand-counted and measured + # independently, both giving +1M -12A. `Thesis/ERRATA.md` records the + # divergence; nothing here says the thesis was wrong. + "33ADD n=0,0": ((66, 3, 75, 12, 1), 2), # Degree 3, post-C4 "13ADD n=1,0": ((22, 2, 43, 7, 1), 2), # Degree 1 and 3, Up Adjust "3DBL n=0": ((73, 3, 101, 19, 1), 2), # Degree 3 "1DBL n=1": ((7, 1, 19, 6, 1), 2), # Degree 1 From df6dbf6683d2cfb501f089f3d1681e9f2c710fb6 Mon Sep 17 00:00:00 2001 From: Sebastian Lindner <33971232+salindne@users.noreply.github.com> Date: Wed, 26 Aug 2026 13:48:04 -0600 Subject: [PATCH 2/4] split g3 nch2 and ch2 additions: the same adjugate trade, +1M -12A each Deg3ADD generic: nch2 65M 3S 85A 0C -> 66M 3S 73A 0C ch2 65M 3S 80A 0C -> 66M 3S 68A 0C Not a copy of the arb edit, because both files had already inlined t3 as -up0*t8*m7 and +up0*t8*m7. That spends two multiplications where arb's t3*m7 spends one, so their determinants cost 4M against arb's 3M and their prefixes reached the same 15M by a different route. The new determinant reads t2*m8 and wants neither t3 nor the inlined pair, so in these two files the conversion drops two multiplications at d and adds three shifts, arriving at the same +1M overall from the other side. No other shape moves in either family. The identities were re-verified with t1, t4, t7 and the modulus all free, 5000 trials over GF(10007), which covers the doublings as well since only the t-recurrence shape matters. Gates: opcount as above, whitebox 7043/7043, driver --strict 55236/55236, dominance clean on 39 files, adjugate ok, selftest 19/0/0, Magma 30 testers 0 failures 0 skips in 5m22s. Deviation from the plan's one-file-per-commit: both specialisations are in this one commit. opcount isolates each family, but whitebox, dominance and Magma cover them jointly, and splitting the commit would imply per-file evidence that was not gathered. --- .../negReduced/g3Formulas/ch2_splitG3_ADD.mag | 46 ++++++++++++------- .../g3Formulas/nch2_splitG3_ADD.mag | 44 ++++++++++++------ 2 files changed, 59 insertions(+), 31 deletions(-) diff --git a/g3/splitModel/negReduced/g3Formulas/ch2_splitG3_ADD.mag b/g3/splitModel/negReduced/g3Formulas/ch2_splitG3_ADD.mag index 46f20cf..83feb5c 100644 --- a/g3/splitModel/negReduced/g3Formulas/ch2_splitG3_ADD.mag +++ b/g3/splitModel/negReduced/g3Formulas/ch2_splitG3_ADD.mag @@ -7964,14 +7964,27 @@ Deg3ADD:= function(u2,u1,u0,v2,v1,v0,up2,up1,up0,vp2,vp1,vp0,ccs) t2 := up0*t7; t5 := t1 + up1*t7; t8 := t4 + up2*t7; - //t3 := +u20*t8; - t6 := t2 + up1*t8; - t9 := t5 + up2*t8; + //t3 := up0*t8; + //t6 := t2 + up1*t8; + //t9 := t5 + up2*t8; - m1 := t5*t9 + t6*t8; - m4 := t6*t7 + t4*t9; + // Bottom row of the adjugate needs only columns 1 and 2 of T. + m9 := t1*t5 + t2*t4; + m8 := t2*t7 + t1*t8; m7 := t4*t8 + t5*t7; - d := t1*m1 + t2*m4 + up0*t8*m7; //Determinant of T = t1*m1 + t2*m4 + t3*m7 + + // Column 3 of T is x*(column 2) reduced mod up, so the remaining six + // adjugate entries are shifts of the bottom row with one multiplication + // each rather than a 2x2 minor. m3, m4 and m6 are the three the + // determinant does not read, so they wait until the generic path below. + m5 := m9 + up2*m8; + m2 := up0*m7; + m1 := m5 + up1*m7; + + // t7*m3 = t7*(up0*m8) = (up0*t7)*m8 = t2*m8. So the determinant needs m8, + // not m3, and the inlined up0*t8*m7 it used to spend two multiplications + // on is gone. + d := t1*m1 + t4*m2 + t2*m8; //Determinant of T if IsZero(d) then //dw1 := u mod up; // = (a1^-1)*S for S,a1,b1 = XGCD(u,up); @@ -9584,16 +9597,17 @@ Deg3ADD:= function(u2,u1,u0,v2,v1,v0,up2,up1,up0,vp2,vp1,vp0,ccs) vt1:= vp1 + v1; vt0:= vp0 + v0; - //Compute s = vt*q mod u2 using Karatsuba twice - t0 := vt0*m1; - t1 := vt1*m4; - t2 := vt2*m7; - t3 := (m4 + m7)*(vt1 + vt2) + t2 + t1 + up2*t2; - t4 := up1*t2; - t5 := t4 + t1; - sp0 := t0 + up0*t3; - sp1 := (m1 + m4)*(vt0 + vt1) + sp0 + (up0 + up1)*(t2 + t3) + t5; - sp2 := (m1 + m7)*(vt0 + vt2) + t0 + t2 + t5 + up2*t3; + // the three adjugate entries the determinant did not need + m4 := m8 + up2*m7; + m6 := m2 + up1*m8; + m3 := up0*m8; + + //Compute s = vt*q mod up as the matrix-vector product sp = M*vt. M is + //multiplication by q = d/u mod up, so this is the same map as vt*q mod up, + //and applying the matrix needs no reduction step. + sp0 := vt0*m1 + vt1*m2 + vt2*m3; + sp1 := vt0*m4 + vt1*m5 + vt2*m6; + sp2 := vt0*m7 + vt1*m8 + vt2*m9; if IsZero(sp2) then if IsZero(sp1) then diff --git a/g3/splitModel/negReduced/g3Formulas/nch2_splitG3_ADD.mag b/g3/splitModel/negReduced/g3Formulas/nch2_splitG3_ADD.mag index 3e5118a..a99554f 100644 --- a/g3/splitModel/negReduced/g3Formulas/nch2_splitG3_ADD.mag +++ b/g3/splitModel/negReduced/g3Formulas/nch2_splitG3_ADD.mag @@ -8099,13 +8099,26 @@ Deg3ADD:= function(u2,u1,u0,v2,v1,v0,up2,up1,up0,vp2,vp1,vp0,ccs) t5 := t1 - up1*t7; t8 := t4 - up2*t7; //t3 := -up0*t8; - t6 := t2 - up1*t8; - t9 := t5 - up2*t8; + //t6 := t2 - up1*t8; + //t9 := t5 - up2*t8; - m1 := t5*t9 - t6*t8; - m4 := t6*t7 - t4*t9; + // Bottom row of the adjugate needs only columns 1 and 2 of T. + m9 := t1*t5 - t2*t4; + m8 := t2*t7 - t1*t8; m7 := t4*t8 - t5*t7; - d := t1*m1 + t2*m4 - up0*t8*m7; //Determinant of T = t1*m1 + t2*m4 + t3*m7 + + // Column 3 of T is x*(column 2) reduced mod up, so the remaining six + // adjugate entries are shifts of the bottom row with one multiplication + // each rather than a 2x2 minor. m3, m4 and m6 are the three the + // determinant does not read, so they wait until the generic path below. + m5 := m9 + up2*m8; + m2 := -up0*m7; + m1 := m5 + up1*m7; + + // t7*m3 = t7*(-up0*m8) = (-up0*t7)*m8 = t2*m8, the signs cancelling because + // t2 and m3 carry the same -up0 factor. So the determinant needs m8, not m3, + // and the inlined -up0*t8*m7 it used to spend two multiplications on is gone. + d := t1*m1 + t4*m2 + t2*m8; //Determinant of T if IsZero(d) then //dw1 := u mod up; // = (a1^-1)*S for S,a1,b1 = XGCD(u,up); @@ -9769,16 +9782,17 @@ Deg3ADD:= function(u2,u1,u0,v2,v1,v0,up2,up1,up0,vp2,vp1,vp0,ccs) vt1:= vp1 - v1; vt0:= vp0 - v0; - //Compute s = vt*q mod u2 using Karatsuba twice - t0 := vt0*m1; - t1 := vt1*m4; - t2 := vt2*m7; - t3 := (m4 + m7)*(vt1 + vt2) - t2 - t1 - up2*t2; - t4 := up1*t2; - t5 := t4 - t1; - sp0 := t0 - up0*t3; - sp1 := (m1 + m4)*(vt0 + vt1) - sp0 - (up0 + up1)*(t2 + t3) + t5; - sp2 := (m1 + m7)*(vt0 + vt2) - t0 - t2 - t5 - up2*t3; + // the three adjugate entries the determinant did not need + m4 := m8 + up2*m7; + m6 := m2 - up1*m8; + m3 := -up0*m8; + + //Compute s = vt*q mod up as the matrix-vector product sp = M*vt. M is + //multiplication by q = d/u mod up, so this is the same map as vt*q mod up, + //and applying the matrix needs no reduction step. + sp0 := vt0*m1 + vt1*m2 + vt2*m3; + sp1 := vt0*m4 + vt1*m5 + vt2*m6; + sp2 := vt0*m7 + vt1*m8 + vt2*m9; if IsZero(sp2) then if IsZero(sp1) then From 9bcf7344babcfd856eeb6067bc41b4b68912af38 Mon Sep 17 00:00:00 2001 From: Sebastian Lindner <33971232+salindne@users.noreply.github.com> Date: Wed, 26 Aug 2026 14:19:01 -0600 Subject: [PATCH 3/4] split g3 doublings: the adjugate trade at all three sites, +1M -12A each Deg3DBL typical: arb 73M 3S 101A 19C -> 74M 3S 89A 19C nch2 72M 4S 97A 0C -> 73M 4S 85A 0C ch2 71M 4S 86A 1C -> 72M 4S 74A 1C Same rewrite as the additions at the other modulus: the doubling reduces mod u where the addition reduces mod up, and its vector is kp rather than vt. The shift identities were re-verified with t1, t4 and t7 free and the modulus free, 5000 trials over GF(10007), so one check covers both operations rather than assuming the addition's result transfers. That completes C4 at all six sites, +6M -72A in total, with no unintended shape moving in any of the twelve families. The block conversion is free, which is worth stating because the plan predicted otherwise. adjugate.py measures both routes from their real .mag text and each costs 15M 0S 9A -- the old one yielding four adjugate entries, the new one seven, because column 3 of T is x*(column 2) reduced mod the modulus and six entries are therefore one multiplication each. The whole +1M is downstream, where applying the matrix costs 12M against Karatsuba's 11M, and the -12A is downstream too: 20A becomes 8A. selftest's published pin for 3DBL n=0 is superseded alongside 33ADD n=0,0, both hand-counted independently of opcount and both agreeing at +1M -12A. Gates: opcount as above, whitebox 7043/7043, driver --strict 55236/55236, dominance clean on 39 files, adjugate ok, selftest 19/0/0, Magma 30 testers 0 failures 0 skips in 4m51s. --- .../negReduced/g3Formulas/arb_splitG3_DBL.mag | 49 ++++++++++++------- .../negReduced/g3Formulas/ch2_splitG3_DBL.mag | 49 ++++++++++++------- .../g3Formulas/nch2_splitG3_DBL.mag | 49 ++++++++++++------- verification/selftest.py | 7 ++- 4 files changed, 96 insertions(+), 58 deletions(-) diff --git a/g3/splitModel/negReduced/g3Formulas/arb_splitG3_DBL.mag b/g3/splitModel/negReduced/g3Formulas/arb_splitG3_DBL.mag index dce8d44..bdb2d82 100644 --- a/g3/splitModel/negReduced/g3Formulas/arb_splitG3_DBL.mag +++ b/g3/splitModel/negReduced/g3Formulas/arb_splitG3_DBL.mag @@ -1163,16 +1163,26 @@ Deg3DBL:= function(u2,u1,u0,v2,v1,v0,ccs) t2 := -u0*t7; t5 := t1 - u1*t7; t8 := t4 - u2*t7; - t3 := -u0*t8; - t6 := t2 - u1*t8; - t9 := t5 - u2*t8; - - //Determinant of T = t1*m1 + t2*m4 + t3*m7 - //First column of M corresponds to polynomial q = m7x^2 + m4x + m1 = d/u1 mod u2. - m1 := t5*t9 - t6*t8; - m4 := t6*t7 - t4*t9; + //t3 := -u0*t8; + //t6 := t2 - u1*t8; + //t9 := t5 - u2*t8; + + // Bottom row of the adjugate needs only columns 1 and 2 of T. + m9 := t1*t5 - t2*t4; + m8 := t2*t7 - t1*t8; m7 := t4*t8 - t5*t7; - d := t1*m1 + t2*m4 + t3*m7; + + // Column 3 of T is x*(column 2) reduced mod u, so the remaining six + // adjugate entries are shifts of the bottom row with one multiplication + // each rather than a 2x2 minor. m3, m4 and m6 are the three the + // determinant does not read, so they wait until the generic path below. + m5 := m9 + u2*m8; + m2 := -u0*m7; + m1 := m5 + u1*m7; + + // t7*m3 = t7*(-u0*m8) = (-u0*t7)*m8 = t2*m8, the signs cancelling because + // t2 and m3 carry the same -u0 factor. So the determinant needs m8, not m3. + d := t1*m1 + t4*m2 + t2*m8; if IsZero(d) then //gcd(h + 2*v1, u1) = (x + xp1) @@ -1418,16 +1428,17 @@ Deg3DBL:= function(u2,u1,u0,v2,v1,v0,ccs) kp0 := dn5 + c3*v0 - h2*v1 - v2*vh1 - u1*k2 - u2*k1 - t12 - t12; - //Compute s = k*q mod u using Karatsuba twice - t0 := kp0*m1; - t1 := kp1*m4; - t2 := kp2*m7; - t3 := (m4 + m7)*(kp1 + kp2) - t2 - t1 - u2*t2; - t4 := u1*t2; - t5 := t4 - t1; - sp0 := t0 - u0*t3; - sp1 := (m1 + m4)*(kp0 + kp1) - sp0 - (u0 + u1)*(t2 + t3) + t5; - sp2 := (m1 + m7)*(kp0 + kp2) - t0 - t2 - t5 - u2*t3; + // the three adjugate entries the determinant did not need + m4 := m8 + u2*m7; + m6 := m2 - u1*m8; + m3 := -u0*m8; + + //Compute s = k*q mod u as the matrix-vector product sp = M*kp. M is + //multiplication by q = d/w mod u, so this is the same map as kp*q mod u, + //and applying the matrix needs no reduction step. + sp0 := kp0*m1 + kp1*m2 + kp2*m3; + sp1 := kp0*m4 + kp1*m5 + kp2*m6; + sp2 := kp0*m7 + kp1*m8 + kp2*m9; if IsZero(sp2) then if IsZero(sp1) then diff --git a/g3/splitModel/negReduced/g3Formulas/ch2_splitG3_DBL.mag b/g3/splitModel/negReduced/g3Formulas/ch2_splitG3_DBL.mag index f9022c2..3a51868 100644 --- a/g3/splitModel/negReduced/g3Formulas/ch2_splitG3_DBL.mag +++ b/g3/splitModel/negReduced/g3Formulas/ch2_splitG3_DBL.mag @@ -1090,16 +1090,26 @@ Deg3DBL:= function(u2,u1,u0,v2,v1,v0,ccs) t2 := u0*t7; t5 := t1 + u1*t7; t8 := t4 + u2*t7; - t3 := u0*t8; - t6 := t2 + u1*t8; - t9 := t5 + u2*t8; - - //Determinant of T = t1*m1 + t2*m4 + t3*m7 - //First column of M corresponds to polynomial q = m7x^2 + m4x + m1 = d/u1 mod u2. - m1 := t5*t9 + t6*t8; - m4 := t6*t7 + t4*t9; + //t3 := u0*t8; + //t6 := t2 + u1*t8; + //t9 := t5 + u2*t8; + + // Bottom row of the adjugate needs only columns 1 and 2 of T. + m9 := t1*t5 + t2*t4; + m8 := t2*t7 + t1*t8; m7 := t4*t8 + t5*t7; - d := t1*m1 + t2*m4 + t3*m7; + + // Column 3 of T is x*(column 2) reduced mod u, so the remaining six + // adjugate entries are shifts of the bottom row with one multiplication + // each rather than a 2x2 minor. m3, m4 and m6 are the three the + // determinant does not read, so they wait until the generic path below. + m5 := m9 + u2*m8; + m2 := u0*m7; + m1 := m5 + u1*m7; + + // t7*m3 = t7*(u0*m8) = (u0*t7)*m8 = t2*m8. So the determinant needs m8, + // not m3. + d := t1*m1 + t4*m2 + t2*m8; if IsZero(d) then //gcd(h + 2*v1, u1) = (x + xp1) @@ -1327,16 +1337,17 @@ Deg3DBL:= function(u2,u1,u0,v2,v1,v0,ccs) kp1 := v0 + yn0 + v2*vn2 + yy2 + u2*k2; kp0 := f3 + h2*v1 + v2*vh1 + u1*kp2 + u2*kp1; - //Compute s = k*q mod u using Karatsuba twice - t0 := kp0*m1; - t1 := kp1*m4; - t2 := kp2*m7; - t3 := (m4 + m7)*(kp1 + kp2) + t2 + t1 + u2*t2; - t4 := u1*t2; - t5 := t4 + t1; - sp0 := t0 + u0*t3; - sp1 := (m1 + m4)*(kp0 + kp1) + sp0 + (u0 + u1)*(t2 + t3) + t5; - sp2 := (m1 + m7)*(kp0 + kp2) + t0 + t2 + t5 + u2*t3; + // the three adjugate entries the determinant did not need + m4 := m8 + u2*m7; + m6 := m2 + u1*m8; + m3 := u0*m8; + + //Compute s = k*q mod u as the matrix-vector product sp = M*kp. M is + //multiplication by q = d/w mod u, so this is the same map as kp*q mod u, + //and applying the matrix needs no reduction step. + sp0 := kp0*m1 + kp1*m2 + kp2*m3; + sp1 := kp0*m4 + kp1*m5 + kp2*m6; + sp2 := kp0*m7 + kp1*m8 + kp2*m9; if IsZero(sp2) then diff --git a/g3/splitModel/negReduced/g3Formulas/nch2_splitG3_DBL.mag b/g3/splitModel/negReduced/g3Formulas/nch2_splitG3_DBL.mag index 6caf2ec..72748e4 100644 --- a/g3/splitModel/negReduced/g3Formulas/nch2_splitG3_DBL.mag +++ b/g3/splitModel/negReduced/g3Formulas/nch2_splitG3_DBL.mag @@ -1111,16 +1111,26 @@ Deg3DBL:= function(u2,u1,u0,v2,v1,v0,ccs) t2 := -u0*t7; t5 := t1 - u1*t7; t8 := t4 - u2*t7; - t3 := -u0*t8; - t6 := t2 - u1*t8; - t9 := t5 - u2*t8; - - //Determinant of T = t1*m1 + t2*m4 + t3*m7 - //First column of M corresponds to polynomial q = m7x^2 + m4x + m1 = d/u1 mod u2. - m1 := t5*t9 - t6*t8; - m4 := t6*t7 - t4*t9; + //t3 := -u0*t8; + //t6 := t2 - u1*t8; + //t9 := t5 - u2*t8; + + // Bottom row of the adjugate needs only columns 1 and 2 of T. + m9 := t1*t5 - t2*t4; + m8 := t2*t7 - t1*t8; m7 := t4*t8 - t5*t7; - d := t1*m1 + t2*m4 + t3*m7; + + // Column 3 of T is x*(column 2) reduced mod u, so the remaining six + // adjugate entries are shifts of the bottom row with one multiplication + // each rather than a 2x2 minor. m3, m4 and m6 are the three the + // determinant does not read, so they wait until the generic path below. + m5 := m9 + u2*m8; + m2 := -u0*m7; + m1 := m5 + u1*m7; + + // t7*m3 = t7*(-u0*m8) = (-u0*t7)*m8 = t2*m8, the signs cancelling because + // t2 and m3 carry the same -u0 factor. So the determinant needs m8, not m3. + d := t1*m1 + t4*m2 + t2*m8; if IsZero(d) then //gcd(h + 2*v1, u1) = (x + xp1) @@ -1368,16 +1378,17 @@ Deg3DBL:= function(u2,u1,u0,v2,v1,v0,ccs) kp0 := f3 - v2*vh1 - u1*k2 - u2*k1 - t12 - t12; - //Compute s = k*q mod u using Karatsuba twice - t0 := kp0*m1; - t1 := kp1*m4; - t2 := kp2*m7; - t3 := (m4 + m7)*(kp1 + kp2) - t2 - t1 - u2*t2; - t4 := u1*t2; - t5 := t4 - t1; - sp0 := t0 - u0*t3; - sp1 := (m1 + m4)*(kp0 + kp1) - sp0 - (u0 + u1)*(t2 + t3) + t5; - sp2 := (m1 + m7)*(kp0 + kp2) - t0 - t2 - t5 - u2*t3; + // the three adjugate entries the determinant did not need + m4 := m8 + u2*m7; + m6 := m2 - u1*m8; + m3 := -u0*m8; + + //Compute s = k*q mod u as the matrix-vector product sp = M*kp. M is + //multiplication by q = d/w mod u, so this is the same map as kp*q mod u, + //and applying the matrix needs no reduction step. + sp0 := kp0*m1 + kp1*m2 + kp2*m3; + sp1 := kp0*m4 + kp1*m5 + kp2*m6; + sp2 := kp0*m7 + kp1*m8 + kp2*m9; if IsZero(sp2) then k3 := T4 + T4; diff --git a/verification/selftest.py b/verification/selftest.py index 3f866f9..3ae6d33 100644 --- a/verification/selftest.py +++ b/verification/selftest.py @@ -1882,7 +1882,12 @@ def section_split_counts(rep, quick): # divergence; nothing here says the thesis was wrong. "33ADD n=0,0": ((66, 3, 75, 12, 1), 2), # Degree 3, post-C4 "13ADD n=1,0": ((22, 2, 43, 7, 1), 2), # Degree 1 and 3, Up Adjust - "3DBL n=0": ((73, 3, 101, 19, 1), 2), # Degree 3 + # SUPERSEDED BY C4, same trade as 33ADD above and for the same + # reason. Published cell (73, 3, 101, 19, 1). Hand-counted and + # measured independently, both giving +1M -12A: the generic path's + # s computation goes 11M 20A -> 12M 8A while the T-block holds at + # 15M 9A. `Thesis/ERRATA.md` records the divergence. + "3DBL n=0": ((74, 3, 89, 19, 1), 2), # Degree 3, post-C4 "1DBL n=1": ((7, 1, 19, 6, 1), 2), # Degree 1 "1DBL n=2": ((14, 3, 25, 8, 1), 2), # Degree 1 with Down Adjust }), From c3f761c8b388016086c1be70dbfff3d9378f8d92 Mon Sep 17 00:00:00 2001 From: Sebastian Lindner <33971232+salindne@users.noreply.github.com> Date: Wed, 26 Aug 2026 14:56:58 -0600 Subject: [PATCH 4/4] C4 documentation: six moved cells, the free adjugate, and two errata Thesis/ERRATA.md E-T10 and the cells themselves in Thesis/chapter6.tex: the genus-3 split Degree-3 rows of tab:g3splitfcostsADD and tab:g3splitfcostsDBL, six cells, +1M -12A each. Not an error in the thesis -- the published counts were right for the formulas as published and the formulas moved under them, which is why Thesis/ must stop quoting a cost the code no longer has. ThesisPublished/ is untouched, verified. NEW_WORK.md N33 states the result as mathematics rather than an operation count. The adjugate of a multiplication matrix inherits the shift structure of the matrix, so with column 3 of T equal to x times column 2 reduced mod the modulus, the bottom row is a cross product needing no third column and the other six entries are shifts at one multiplication each. All nine entries therefore cost the same 15M 0S 9A as the three the old arrangement built, and the whole trade is downstream: 12M 8A to apply the matrix against Karatsuba's 11M 20A. It also records the refutation. The plan hand-counted the block conversion as losing 2M 1A and my own independent hand count said 0M; the measurement says the block is free and the total is +1M. adjugate.py settles it by executing both routes from their real .mag text, and keeps the pre-C4 route as split_q_col1 so the comparison keeps scoring what C4 measured itself against. ERRATA.md E23: the gates mutate formula files in place to provoke their own guards, so they cannot be run concurrently, nor alongside Magma which loads those files. Found by making the mistake: two selftest runs raced and left the ramified ADD missing the ledger comment adjugate anchors on, surfacing as a gate failure naming a file nothing had edited. Not fixed; the symptom is recorded so the next person does not debug a phantom formula defect. ERRATA.md E24: C5, dropped from this PR. The Bezout cofactor identity is proved -- the closed form is proportional to the true S*dw2^-1 mod unp in 400 of 400 constructed trials -- but the file's (b1,b0) is not that quantity up to scalar in any of them, so its normalisation is unknown and the substitution cannot be written. Recorded rather than applied, per the rule for a result proved before its oracle exists, with the two leads, the nine unexamined sibling sites, and a warning not to re-attempt the Python reproduction whose transcription is the thing in doubt. README: the six genus-3 split cells in Typical Case Operation Counts, the four in the Related Work genus-3 split table, and the two appendix rows, each re-measured by opcount rather than copied. The exactness claim now names its two deliberate exceptions instead of overstating. Gates: readme-paths 94/94, check_paths --strict 259/259, ruff clean on CI's ruleset, opcount confirming all six figures as written. --- ERRATA.md | 110 ++++++++++++++++++++++++++++++++++++++++++ NEW_WORK.md | 114 ++++++++++++++++++++++++++++++++++++++++++++ README.md | 20 ++++---- Thesis/ERRATA.md | 38 +++++++++++++++ Thesis/chapter6.tex | 4 +- 5 files changed, 274 insertions(+), 12 deletions(-) diff --git a/ERRATA.md b/ERRATA.md index 4c9edfa..624c5ff 100644 --- a/ERRATA.md +++ b/ERRATA.md @@ -1062,3 +1062,113 @@ the two versions cannot drift. Magma now passes all three. basis-aware either, so a posReduced run wrote over the negReduced run's log. Worse than untidy: `--inherit-from` reads those logs, so a negReduced arb could have been handed posReduced cases. Found by noticing a POS log where a NEG one belonged. + +## E23: the gates mutate formula files, so they cannot be run concurrently + +**Found 2026-08-26 during C4, by making the mistake.** `verification/selftest.py` +provokes its own guards by editing formula files in place and restoring them: +`blocks` drops a live `ExactQuotient` from `arb_splitG3_ADD.mag`, `dominance` +deletes a live `k3` assignment, `adjugate` removes a ledger comment. Each run +restores what it touched, and each is correct on its own. + +**Two runs at once are not.** A second `selftest` restored a file while the first +still held it mutated, and the restore was lost. The tree was left with +`g3/ramifiedModel/g3Formulas/arb_ramifiedG3_ADD.mag` missing the six-line +`// top: 15m 0s 9a` ledger comment that `adjugate.py` anchors on -- a file +neither run was testing and no one had edited. + +**It surfaced as a gate failure naming an untouched file:** `adjugate` reporting +`arb_ramifiedG3_ADD.mag no longer carries its 'top' op-count comment, so +shipped_7 is measured against nothing`, while a standalone `adjugate` run passed. +That is a confusing signal, and the natural first hypothesis -- that the edit +under test broke something -- is wrong. + +**The same hazard applies to Magma.** `./test_all.sh` loads formula files as each +tester starts, so a `selftest` running alongside it can feed a deliberately +broken file to a tester, and the suite's verdict then describes neither the +committed code nor the code under test. + +**Not fixed, and the fix is not obvious.** Restoring via a temporary copy rather +than in place would remove the hazard, but the provocations must edit the real +path because the gates locate their targets by content in the real file. A lock +file would serialise them at the cost of silent waiting. Recorded so the next +person recognises the symptom rather than debugging a phantom formula defect. + +**Meanwhile: run the gates serially, and never alongside Magma.** If a gate fails +naming a file the current work did not touch, check `git status` before believing +it. + +## E24: a proved saving in the split genus-3 addition that cannot yet be applied + +**Registered 2026-08-26, when C5 was dropped from the C4 pull request.** Not a +defect in the formulas -- a saving that is mathematically established and whose +implementation is blocked on a question about the code, recorded per the standing +rule that a result proved before its oracle exists is written down rather than +applied. + +**The setting.** In `F_q[x]`, one Euclidean division step on `(up, dw2)` gives +`up = q*dw2 + dw3` with `deg dw3 < deg dw2`. That is the first step of the +extended Euclidean algorithm on the pair, and the extended algorithm maintains +Bezout cofactors alongside the remainders: with `s_i*up + t_i*dw2 = r_i` and the +recurrences `s_{i+1} = s_{i-1} - q_i*s_i`, `t_{i+1} = t_{i-1} - q_i*t_i`, after +one step `t_1 = -q`. So the cofactor of `dw2` is, up to sign, exactly the +quotient the division already computed. Reducing modulo `up` gives +`t_1*dw2 = dw3 (mod up)`, hence `dw2^{-1} = t_1*dw3^{-1} (mod up)` whenever +`dw3` is a unit there. + +**The saving.** In the leaf of `Deg3ADD` where the guards force `d = 0`, +`t7 = 0` and `t4 = 0`, the formulas need `b2 = S*(dw2^{-1} mod up)` with `S` the +monic `dw3`, and spend **11M 6A** on a `2x2` Cramer solve, its own comment naming +the method: `//b2 := S*R!((Q!a)^-1) mod up; //2x2 system`. By the above the +polynomial part is free, and the intermediate it needs is already in hand: +`t0 := w2*up2 - w1` is computed twelve lines earlier as part of +`//dw3 := up mod dw2;`. + +**What is proved.** Constructing inputs that satisfy the leaf's precondition -- +`S1 = x + a`, `dw2 = S1*(bx + c)`, `q = (1/b)x + g`, `up = q*dw2 + lam*S1`, so +`S1 | up` and `up mod dw2 = lam*S1` -- and computing the true +`S*dw2^{-1} mod unp` independently in the quotient ring: the closed form +`(-w2, -t0)` is proportional to it in **400 of 400** trials over GF(1000003). +Constructed rather than sampled because rejection sampling reaches this leaf +about once in ten thousand random inputs. + +**What blocks it.** The file's `(b1, b0)` is **not** that quantity up to any +scalar, 0 of 400. So the substitution is not `b1 := -w2; b0 := -t0` at some +weight, and the file's normalisation of `b2` is unknown. Two leads: +`b1 := m1 + w4*m2` and `b0 := m3 + w4*m4` form `M*(1,w4)^T` for +`M = [[m1,m2],[m3,m4]]`, which is not a Cramer solve for an inverse; and the +comment `//a := dw2 mod up;` does not say whether the modulus is the original +degree-3 `up` or the degree-2 quotient built two lines above, with the file's +`up` still holding the degree-3 one at that point. + +**Do not settle this by re-implementing the block in Python.** That was tried. +The transcription is precisely what is in doubt, so the experiment cannot +distinguish its own error from a real difference. Observe the real execution +instead: a recursive copy of `maginterp`'s statement loop in the manner of +`detect.py`, snapshotting the environment where `b0` lands, over the whitebox +cases reaching `ADD281`, `ADD282` and `ADD283`. Note `opcount.py` cannot verify +such an edit -- the leaf is not a priced row -- so `whitebox.py` and Magma would +carry it alone. The corpus **can** see the leaf: breaking `b1` deliberately gives +9 whitebox mismatches, checked 2026-08-26. + +**Worth little in expectation, which is why it was dropped rather than finished.** +Three zero-tests deep and reached about once in ten thousand inputs, so the +11M 6A is a raw count on a path almost never taken, and no published cell moves. + +**Nine sibling sites are unexamined**, each a `b2` computation of the same family: +`arb_splitG3_ADD.mag:8113/8249/9312`, `nch2_splitG3_ADD.mag:7834/7970/9031`, +`ch2_splitG3_ADD.mag:7696/7832/8871` (numbering predates C4; locate by content). +Each needs a division producing the remainder just above it and needs `u = up`. +Genus-2 split has no `b2` site at all, and no doubling has one. + +**The transferable rule, which outlives this leaf.** Wherever an explicit formula +computes a remainder `r = a mod b` and later requires the Bezout cofactor of `b` +modulo `a` -- equivalently an inverse of `b` in `F_q[x]/(a)` -- the quotient +discarded by that division already is that cofactor, up to the scalar `lc(r)`. +The ramified model exploits this and the split model does not, and the reason is +inversion scheduling rather than oversight: ramified inverts early, so `S1` +becomes an exact scalar and everything downstream is unweighted, while split +inverts late because its `f` is non-monic of degree `2g+2` and `upp` must be +normalised after `upp` is known, which is how it holds to a single inversion. +Late inversion means every upstream quantity is carried with a weight, and a +weighted cofactor is not a drop-in for an exact one. diff --git a/NEW_WORK.md b/NEW_WORK.md index 2f0d471..c222405 100644 --- a/NEW_WORK.md +++ b/NEW_WORK.md @@ -2958,3 +2958,117 @@ Two standing rules, both learned the hard way here: alternative is that the next person re-runs the experiment. - **State honest limits in the entry, not in a footnote.** E-T5 is not measured and says so; the doubling composite is approximate and says so. + +## N33 — The adjugate is nearly free, and a discarded quotient is a Bezout cofactor + +**Status** — established, C4. **Where** — `g3/splitModel/negReduced/g3Formulas/`, +all six `Deg3ADD` and `Deg3DBL` files; the published tables are +`Thesis/chapter6.tex` `tab:g3splitfcosts{ADD,DBL}`, corrected as `E-T10`. + +**What was there.** Composing two degree-3 divisors needs `s = vt*q mod up`, +where `q = d/w mod up` is a quadratic whose coefficient vector is the first +column of the adjugate of the `3x3` matrix `T`. The split formulas built exactly +that column -- three `2x2` minors `m1`, `m4`, `m7` -- took the determinant by +expanding along `T`'s first row, and then reduced the product `vt*q` modulo `up` +using Karatsuba twice. + +**The finding, and it is not the one the plan predicted.** Carrying the *whole* +adjugate and applying it as a matrix-vector product is `+1M -12A` on the generic +path of all six operations. The plan expected the extra multiplication to be paid +at the `T` block, on the reasoning that nine entries must cost more than three. +Measured, the block costs **`15M 0S 9A` either way**. + +The reason is structural rather than a happy accident, and it is the part worth +publishing. Column 3 of `T` is `x` times column 2, reduced modulo `up`. `adj(T)` +is itself a multiplication matrix -- by `q`, up to `det(T)` -- so it inherits that +shift structure. Concretely, the bottom row `(m7, m8, m9)` is the cross product +of columns 1 and 2 of `T` and needs no third column at all, and the remaining six +entries are then **shifts of that row costing one multiplication each** instead +of a `2x2` minor costing two: + + m5 = m9 + w2*m8 m2 = -w0*m7 m1 = m5 + w1*m7 + m4 = m8 + w2*m7 m6 = m2 - w1*m8 m3 = -w0*m8 + +with `(w0, w1, w2)` the modulus coefficients. Three minors at `2M` and six shifts +at `1M` is `12M`, exactly what three minors at `2M` plus the three now-unneeded +`T` entries `t3`, `t6`, `t9` at `1M` used to cost. The nine-entry adjugate is +free relative to the three-entry column. + +A further multiplication falls out of `t7*m3 = (-w0*t7)*m8 = t2*m8`, the signs +cancelling because `t2` and `m3` carry the same `-w0` factor. So the determinant +can be expanded along `T`'s first *column* reading `m8` in place of `m3`, and +`m3` is left wanted only by the generic path, which computes it there -- no +degenerate leaf pays for an entry it never reads. + +**Where the cost actually moves.** Downstream. Applying the matrix is +`9M 6A` plus the three deferred entries at `3M 2A`, so `12M 8A`, against +Karatsuba's `11M 20A`. One multiplication on, twelve additions off, and no +reduction step because applying the multiplication matrix *is* the reduction. + +**Measured, six sites, `+1M -12A` at every one:** + +| | addition | doubling | +|---|---|---| +| arb | 65/3/87/12 -> **66/3/75/12** | 73/3/101/19 -> **74/3/89/19** | +| nch2 | 65/3/85/0 -> **66/3/73/0** | 72/4/97/0 -> **73/4/85/0** | +| ch2 | 65/3/80/0 -> **66/3/68/0** | 71/4/86/1 -> **72/4/74/1** | + +`+6M -72A` in total, accepted comfortably by the thesis's own `1M : 3A` rule. No +other shape moved in any of the twelve families, which is the acceptance test: +the trade is confined to the generic path it targets. + +**Scope, and why it is exactly six.** The `3x3` multiplication matrix exists only +when reducing modulo a degree-3 modulus, so the lower-degree additions and +doublings carry a smaller system with nothing to trade, and genus-2 split has no +such matrix at all -- `m7` occurs zero times in all six of its formula files. + +**For the paper.** State the shift structure as the result, not the `+1M -12A`. +The operation count is an artefact of one model at one genus; the statement that +*the adjugate of a multiplication matrix inherits the shift structure of the +matrix, so all nine entries cost barely more than one column* is what transfers, +and it is what makes the matrix-vector form cheaper than forming the polynomial +and reducing. + +### The refutation this replaced + +The plan carried a hand count saying the conversion **loses `2M 1A`** at the +block and that the `+1M` is paid there. That count is wrong, and it was wrong in +a way no amount of re-reading would have caught: it priced the ramified route's +seven entries against split's three without noticing both arrangements spend the +same fifteen multiplications. `verification/adjugate.py` settles it by executing +both from their real `.mag` text -- `split_q_col1` at `15M 0S 9A` for four +entries, `split_q` at `15M 0S 9A` for seven -- and the pre-C4 route is kept as a +candidate rather than deleted so the comparison keeps scoring it. + +**The methodological point, which this project keeps relearning:** state what a +change *removes*, and measure the total. A hand count of two arrangements is a +prediction, not a result, and here the prediction had the right bottom line for +the wrong reason. My own independent hand count also said `0M` before the +measurement said `+1M`. + +### A related result, proved and deliberately not applied + +The same leaf structure exposed that split spends `11M 6A` reconstructing a +Bezout cofactor it has already computed. The mathematics is settled -- `400 of +400` constructed trials against ground truth in the quotient ring -- and the +implementation is blocked on the file's normalisation of `b2`, so it is recorded +in `ERRATA.md` **E24** rather than applied. The general statement belongs here +because it outlives the leaf: + +**Wherever an explicit formula computes a remainder `r = a mod b` and later needs +the Bezout cofactor of `b` modulo `a`, equivalently an inverse of `b` in +`F_q[x]/(a)`, the quotient discarded by that division already is that cofactor, +up to the scalar `lc(r)`.** One Euclidean step gives `t_1 = -q` in the extended +algorithm's recurrence `t_{i+1} = t_{i-1} - q_i*t_i`, and reducing +`s_1*a + t_1*b = r` modulo `a` gives `b^{-1} = t_1*r^{-1} (mod a)`. The cost of +the modular inverse collapses to the cost of inverting a leading coefficient. + +The ramified model exploits this; the split model does not. The reason is +**inversion scheduling**, and that is the publishable observation: ramified +inverts early, so the monic-making scalar is exact and everything downstream is +unweighted, while split must invert late because its `f` is non-monic of degree +`2g+2` and `upp` needs normalising only after `upp` is known -- batching that +normalisation with weight removal is precisely how the split formulas hold to a +single inversion. Late inversion means every upstream quantity is carried +projectively, and a weighted cofactor is not a drop-in for an exact one. The +omission is a consequence of a deliberate design choice, not an oversight. diff --git a/README.md b/README.md index 6dee0b4..8190eca 100644 --- a/README.md +++ b/README.md @@ -31,15 +31,15 @@ The frequent case of the highest-degree operation in each family. "Frequent" me g3 ramified arb533711544804 g3 ramified nch2533590535610 g3 ramified ch2513620514552 -g3 split arb653871273310119 -g3 split nch2653850724970 -g3 split ch2653800714861 +g3 split arb66375127438919 +g3 split nch2663730734850 +g3 split ch2663680724741 Every figure is measured. `python3 verification/opcount.py --family ` executes the formulas over a real field, identifies the frequent case by observing which branch is taken, and cross-checks each contributing call against the Cantor reference implementation. All fifteen families measure, the twelve above plus genus-2 split negative reduced. -Where the thesis publishes a cell, measurement reproduces it exactly: the genus-2 ramified rows against `tab:ramfcosts`, and 168 split shapes against `tab:splitfcosts` and `tab:g3splitfcosts{ADD,DBL}`, every one at exactly one inversion. One systematic divergence turned up during that check and was the tool's rather than the thesis's, a flat `+2A` on every split row, because a divisor's balancing weight is a small integer and `n := n1 + n2 - 2` is bookkeeping rather than field arithmetic. See [NEW_WORK.md](NEW_WORK.md) N31. +Where the thesis publishes a cell, measurement reproduces it exactly: the genus-2 ramified rows against `tab:ramfcosts`, and 168 split shapes against `tab:splitfcosts` and `tab:g3splitfcosts{ADD,DBL}`, every one at exactly one inversion. Two of those cells have since moved by design: the genus-3 split `33ADD n=0,0` and `3DBL n=0` each trade one multiplication for twelve additions, so measurement and the published table differ there deliberately, recorded in [Thesis/ERRATA.md](Thesis/ERRATA.md). One systematic divergence turned up during the original check and was the tool's rather than the thesis's, a flat `+2A` on every split row, because a divisor's balancing weight is a small integer and `n := n1 + n2 - 2` is bookkeeping rather than field arithmetic. See [NEW_WORK.md](NEW_WORK.md) N31. The genus-2 split figures are positive reduced, the basis of record. Negative reduced is a different algorithm and differs by an operation or two on several rows. @@ -119,10 +119,10 @@ Against Rezai Rad et al. 2019 and Sutherland 2019. MSACMSACMSAC -3DBL odd85216307481270724970 -3ADD odd75213807361270653850 -3DBL char 28911160n/a714861 -3ADD char 28101180n/a653800 +3DBL odd85216307481270734850 +3ADD odd75213807361270663730 +3DBL char 28911160n/a724741 +3ADD char 28101180n/a663680 @@ -544,12 +544,12 @@ Operation costs for every non-degenerate function, measured as in [Typical Case 22ADD n=1,1371567362570334520 23ADD n=0,07538918725783724767 23ADD n=1,0411593411570411550 -33ADD n=0,06538712653850653800 +33ADD n=0,06637512663730663680 1DBL n=042566274065773874410 1DBL n=1711967115371143 1DBL n=2143258143243123194 2DBL n=06259531607894607807 2DBL n=13705613352543361483 -3DBL n=073310119724970714861 +3DBL n=07438919734850724741 diff --git a/Thesis/ERRATA.md b/Thesis/ERRATA.md index f9f9500..598b7e5 100644 --- a/Thesis/ERRATA.md +++ b/Thesis/ERRATA.md @@ -343,3 +343,41 @@ from the `f4`-free code, which is why the contradiction is with the sentence and the numbers. --- + +## E-T10 — the genus-3 split Degree-3 rows trade one multiplication for twelve additions + +**`chapter6.tex:2389` in `tab:g3splitfcostsDBL` and `:2491` in `tab:g3splitfcostsADD`.** +**Measured, and hand-counted independently.** + +**This is not an error in the thesis.** The published counts were correct for the +formulas as published; the formulas have since changed under them. The entry +exists because `Thesis/` must not quote a cost the code no longer has. + + :2389 &73&3&101&19 &72&4&97&0 &71&4&86&1 becomes + &74&3& 89&19 &73&4&85&0 &72&4&74&1 + + :2491 &65&3& 87&12 &65&3&85&0 &65&3&80&0 becomes + &66&3& 75&12 &66&3&73&0 &66&3&68&0 + +Six cells, `+1M -12A` in each, across the `arb`, `nch2` and `ch2` columns of both +tables. S and C are unchanged everywhere, and no other row in either table moves. + +**What changed.** The addition and doubling now carry the full adjugate of the +`3x3` matrix `T` and apply it as a matrix-vector product, where they previously +built only its first column and reduced `vt*q mod up` by Karatsuba twice. The +trade is `+1M` for `-12A`, which the thesis's own `1M : 3A` rule accepts +comfortably. + +**Where the cost actually moves, which the derivation should state.** Not at the +`T` block: that costs `15M 0S 9A` either way, yielding three adjugate entries in +the old arrangement and seven in the new, because column 3 of `T` is `x` times +column 2 reduced modulo the modulus, so six of the nine entries are one +multiplication each rather than a `2x2` minor. The whole `+1M` and the whole +`-12A` are downstream, where applying the matrix costs `12M 8A` against +Karatsuba's `11M 20A`. + +**Adjudicated per the standing rule.** `verification/opcount.py` measures the new +figures by execution and a hand count of the changed region reproduces the same +`+1M -12A`, two methods sharing no code. `verification/selftest.py`'s published +pins for `33ADD n=0,0` and `3DBL n=0` are updated with the reason recorded +inline, and every other pinned cell still matches its published value. diff --git a/Thesis/chapter6.tex b/Thesis/chapter6.tex index ad8e4f3..2323845 100644 --- a/Thesis/chapter6.tex +++ b/Thesis/chapter6.tex @@ -2386,7 +2386,7 @@ \subsection{Field Operation Costs and Comparisons} &62&5&95&31 &60&7&89&4 &60&7&80&7\TS\\ \hline Degree 3 -&73&3&101&19 &72&4&97&0 &71&4&86&1\TS\\ +&74&3&89&19 &73&4&85&0 &72&4&74&1\TS\\ \hline \end{tabular} \end{table} @@ -2488,7 +2488,7 @@ \subsection{Field Operation Costs and Comparisons} &75&3&89&18 &72&5&78&3 &72&4&76&7\TS\\ \hline Degree 3 -&65&3&87&12 &65&3&85&0 &65&3&80&0\TS\\ +&66&3&75&12 &66&3&73&0 &66&3&68&0\TS\\ \hline \end{tabular} \end{table}