Skip to content

Commit 5660088

Browse files
fix: Eisenstein Half-GCD convergence (#680)
Co-authored-by: Ivo Kubjas <[email protected]>
1 parent 1873045 commit 5660088

File tree

2 files changed

+125
-7
lines changed

2 files changed

+125
-7
lines changed

field/eisenstein/eisenstein.go

Lines changed: 64 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,27 @@ type ComplexNumber struct {
99
A0, A1 *big.Int
1010
}
1111

12+
// ──────────────────────────────────────────────────────────────────────────────
13+
// helpers – hex-lattice geometry & symmetric rounding
14+
// ──────────────────────────────────────────────────────────────────────────────
15+
16+
// six axial directions of the hexagonal lattice
17+
var neighbours = [][2]int64{
18+
{1, 0}, {0, 1}, {-1, 1}, {-1, 0}, {0, -1}, {1, -1},
19+
}
20+
21+
// roundNearest returns ⌊(z + d/2) / d⌋ for *any* sign of z, d>0
22+
func roundNearest(z, d *big.Int) *big.Int {
23+
half := new(big.Int).Rsh(d, 1) // d / 2
24+
if z.Sign() >= 0 {
25+
return new(big.Int).Div(new(big.Int).Add(z, half), d)
26+
}
27+
tmp := new(big.Int).Neg(z)
28+
tmp.Add(tmp, half)
29+
tmp.Div(tmp, d)
30+
return tmp.Neg(tmp)
31+
}
32+
1233
func (z *ComplexNumber) init() {
1334
if z.A0 == nil {
1435
z.A0 = new(big.Int)
@@ -124,19 +145,55 @@ func (z *ComplexNumber) Norm() *big.Int {
124145
return norm
125146
}
126147

127-
// QuoRem sets z to the quotient of x and y, r to the remainder, and returns z and r.
148+
// QuoRem sets z to the Euclidean quotient of x / y, r to the remainder,
149+
// and guarantees ‖r‖ < ‖y‖ (true Euclidean division in ℤ[ω]).
128150
func (z *ComplexNumber) QuoRem(x, y, r *ComplexNumber) (*ComplexNumber, *ComplexNumber) {
129-
norm := y.Norm()
130-
if norm.Cmp(big.NewInt(0)) == 0 {
151+
152+
norm := y.Norm() // > 0 (Eisenstein norm is always non-neg)
153+
if norm.Sign() == 0 {
131154
panic("division by zero")
132155
}
133-
z.Conjugate(y)
134-
z.Mul(x, z)
135-
z.A0.Div(z.A0, norm)
136-
z.A1.Div(z.A1, norm)
156+
157+
// num = x * ȳ (ȳ computed in a fresh variable → y unchanged)
158+
var yConj, num ComplexNumber
159+
yConj.Conjugate(y)
160+
num.Mul(x, &yConj)
161+
162+
// first guess by *symmetric* rounding of both coordinates
163+
q0 := roundNearest(num.A0, norm)
164+
q1 := roundNearest(num.A1, norm)
165+
z.A0, z.A1 = q0, q1
166+
167+
// r = x – q*y
137168
r.Mul(y, z)
138169
r.Sub(x, r)
139170

171+
// If Euclidean inequality already holds we're done.
172+
// Otherwise walk ≤2 unit steps in the hex lattice until N(r) < N(y).
173+
if r.Norm().Cmp(norm) >= 0 {
174+
bestQ0, bestQ1 := new(big.Int).Set(z.A0), new(big.Int).Set(z.A1)
175+
bestR := new(ComplexNumber).Set(r)
176+
bestN2 := bestR.Norm()
177+
178+
for _, dir := range neighbours {
179+
candQ0 := new(big.Int).Add(z.A0, big.NewInt(dir[0]))
180+
candQ1 := new(big.Int).Add(z.A1, big.NewInt(dir[1]))
181+
var candQ ComplexNumber
182+
candQ.A0, candQ.A1 = candQ0, candQ1
183+
184+
var candR ComplexNumber
185+
candR.Mul(y, &candQ)
186+
candR.Sub(x, &candR)
187+
188+
if candR.Norm().Cmp(bestN2) < 0 {
189+
bestQ0, bestQ1 = candQ0, candQ1
190+
bestR.Set(&candR)
191+
bestN2 = bestR.Norm()
192+
}
193+
}
194+
z.A0, z.A1 = bestQ0, bestQ1
195+
r.Set(bestR) // update remainder and retry; Euclidean property ⇒ ≤ 2 loops
196+
}
140197
return z, r
141198
}
142199

field/eisenstein/eisenstein_test.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"crypto/rand"
55
"math/big"
66
"testing"
7+
"time"
78

89
"github.com/leanovate/gopter"
910
"github.com/leanovate/gopter/prop"
@@ -240,6 +241,66 @@ func TestEisensteinHalfGCD(t *testing.T) {
240241
properties.TestingRun(t, gopter.ConsoleReporter(false))
241242
}
242243

244+
func TestEisensteinQuoRem(t *testing.T) {
245+
t.Parallel()
246+
parameters := gopter.DefaultTestParameters()
247+
if testing.Short() {
248+
parameters.MinSuccessfulTests = nbFuzzShort
249+
} else {
250+
parameters.MinSuccessfulTests = nbFuzz
251+
}
252+
253+
properties := gopter.NewProperties(parameters)
254+
genE := GenComplexNumber(boundSize)
255+
256+
properties.Property("QuoRem should be correct", prop.ForAll(
257+
func(a, b *ComplexNumber) bool {
258+
var z, rem ComplexNumber
259+
z.QuoRem(a, b, &rem)
260+
var res ComplexNumber
261+
res.Mul(b, &z)
262+
res.Add(&res, &rem)
263+
return res.Equal(a)
264+
},
265+
genE,
266+
genE,
267+
))
268+
269+
properties.Property("QuoRem remainder should be smaller than divisor", prop.ForAll(
270+
func(a, b *ComplexNumber) bool {
271+
var z, rem ComplexNumber
272+
z.QuoRem(a, b, &rem)
273+
return rem.Norm().Cmp(b.Norm()) == -1
274+
},
275+
genE,
276+
genE,
277+
))
278+
}
279+
280+
func TestRegressionHalfGCD1483(t *testing.T) {
281+
// This test is a regression test for issue #1483 in gnark
282+
a0, _ := new(big.Int).SetString("64502973549206556628585045361533709077", 10)
283+
a1, _ := new(big.Int).SetString("-303414439467246543595250775667605759171", 10)
284+
c0, _ := new(big.Int).SetString("-432420386565659656852420866390673177323", 10)
285+
c1, _ := new(big.Int).SetString("238911465918039986966665730306072050094", 10)
286+
a := ComplexNumber{A0: a0, A1: a1}
287+
c := ComplexNumber{A0: c0, A1: c1}
288+
289+
ticker := time.NewTimer(time.Second * 3)
290+
doneCh := make(chan struct{})
291+
go func() {
292+
HalfGCD(&a, &c)
293+
close(doneCh)
294+
}()
295+
296+
select {
297+
case <-ticker.C:
298+
t.Error("HalfGCD took too long to compute")
299+
case <-doneCh:
300+
// Test passed
301+
}
302+
}
303+
243304
// GenNumber generates a random integer
244305
func GenNumber(boundSize int64) gopter.Gen {
245306
return func(genParams *gopter.GenParameters) *gopter.GenResult {

0 commit comments

Comments
 (0)