1

Is there an efficient algorithm to compute the Jacobsthal matrix [WP] or equivalently the quadratic character χ in GF(q),

J [ i, j ] = χ ( i - j ) = 0 if i = j else 1 if i - j is a square in GF(q) else -1,

where i, j run over the elements of GF(q)?

The order of the elements <=> rows/columns does not really matter, so it's mainly to know whether an element of GF(q) is a square. Unfortunately, when q = p n with n > 1, one cannot just take i, jZ/qZ (which works well iff q is a prime <=> n = 1).

On the other hand, implementing arithmetics in GF(q) appears a nontrivial task to me, at least the naive way (constructing an irreducible polynomial P of degree n over Z/pZ and implementing multiplication through multiplication of polynomials modulo P...).

The problem is easily solved in Python using the galois package (see here), but this is quite heavy artillery which I'd like to avoid to deploy. Of course dedicated number theory software may also have GF arithmetics implemented. But I needed this just to produce Hadamard matrices through the Paley construction [WP], so I'd like to be able to compute this without using sophisticated software (and anyway I think it would be interesting to know whether there's a simple algorithm to do this).

Since we only need to know which elements are squares, I hoped there might be an efficient way to determine that.

EDIT: Let me clarify again that the question is whether there exists an efficient way of implementing this function (for arbitrary q = p k) without implementing general arithmetic in GF(q). It's not difficult to solve the problem using dedicated software: For example, Python's galois package provides the is_quadratic_residue() function which immediately gives the matrix elements - in spite of its name, since quadratic residues (mod p^k) aren't the same as squares in GF(p^k): Indeed, default modular arithmetic, i.e., issquare(Mod(i-j, p^k)), will usually yield incorrect results for when k > 1. For example, in G(2^k) every element is a square, but 2 and 3 aren't squares mod 2^2). A crude check is to compute J JT which should equal q I - U (for p > 2) where U is the "all 1s" matrix.)

Max
  • 415
  • 5
  • 12
  • That table is exactly what we want to compute, the question is: **how** to compute the squares (or, more general but a priori more complicated, an arbitrary product), or whether it's possible to know whether one has a square without computing all of them. Maybe based on the list of squares in GF(p) which is easy to do. – Max Jun 05 '22 at 22:19
  • Since the table generation is a one time calculation, then the squares table entries would be | for (i = 0; i < q; i++) | table[i] = -1) | for (i = 0; i < q; i++) | j = gfmpy(i*i) | table[j] = 1 | table[0] = 0 | . The big tables would be matrices [q][q] for add, subtract, multiply, divide, or tables could be used to map integers into to a vector of coefficients and back for the basic math. – rcgldr Jun 06 '22 at 02:04
  • What is `gfmpy(i*i)`? The question is exactly: how to implement a lightweight multiplication function that works (at least) for computing squares in GF(q). It is written in the question that the problem is trivially solved using the (quite heavy) Galois package, which i wanted to avoid. – Max Jun 07 '22 at 20:46
  • gfmpy(i*i) is the name I used for a Galois - finite field multiply function. This could be used for a one time creation of the squares table. – rcgldr Jun 07 '22 at 21:22
  • OK, I guessed that. But the question is precisely: how to compute squares (if it cannot be avoided) without having to implement general arithmetics (i.e., multiplication) in GF(q). – Max Jun 08 '22 at 12:52
  • Can you implement `gfmpy(i, j)` or at least `gfsqr(i)` in a few lines for arbitrary q = p^n ? That's what I'm asking for , unless there's a better way to know whether a given element is a square. – Max Jun 08 '22 at 12:58
  • I've only done specific and simple cases so far, like GF(3^4) or GF(7^2), for Reed Solomon error correction code. For my code, I stored data as integers, and mapped from integers to arrays of coefficients and back as needed. I haven't tried an arbitrary q = p^n case yet. Is q = p^n to be determined at compile time (via defines) or run time (via variables)? – rcgldr Jun 08 '22 at 18:22
  • How do you implement multiplication for these cases? I think for 3^4 the table can be found in Wikipedia, but otherwise...? Do you multiply arrays of coefficients using the product of polynomials (c_k=sum a_i b_{k-i}) and then use euclidean division to take mod the irreducible polynomial whose root is a generator? I can do that (if there's no better way) but then the main question is: How do you get that irreducible polynomial?(x^n+x+1 seems to work often but not always.) I would like to provide a function to get Jacobsthal/Hadamard matrix for arbitrary size so yes, q is only known on runtime. – Max Jun 09 '22 at 17:18
  • 1
    I added an answer showing hard coded math for GF(3^4). Rather than look for an irreducible polynomial, I did a brute for search for a primitive polynomial, where all non-zero numbers are powers of primitive element x + 0, which in this case is 3, the code for this is at the end of my answer. – rcgldr Jun 09 '22 at 23:41
  • 1
    @Max I'm not aware of an algorithm to determine the squares of GF(p^m) with m > 1 without using finite field multiplication. I'm the author of the `galois` library you mentioned. Just curious, are you suggesting the `is_quadratic_residue()` method name is misleading and that it should perhaps be `is_square()` instead? Thanks. – Matt Hostetter Jul 20 '22 at 14:58
  • Hi @Matt, congrats for your library and thanks for dropping by here. Yes, I believe "is_square" would be more correct than "is_quadratic_residue". Because the element of GF(q) really *is* a square, not a residue (which means, remainder mod q). Cf. also https://en.wikipedia.org/wiki/Quadratic_residue : "In number theory, an **integer** k is called quadratic residue **modulo n**, if ...". This is not what we have in GF(q). We really have an element y of the field which is the square of an other element x of the firld, y = x². There is no integer nor modulo here. – Max Jul 23 '22 at 08:03

1 Answers1

0

Here is a basic math table setup for GF(3^4) based on 1 x^4 + 1 x^3 + 1 x^2 + 2 x + 2. At the end of this answer is a brute force search for any primitive polynomial (where powers of 3 map all non-zero elements). Numbers are stored as integer equivalents, for example, x^3 + 2 x + 1 = 1*(3^3) + 2*(3) + 1 = 16, so I store this as 16. Add and subtract just map from integer to vector and back. Multiply and divide use exp and log tables. Exp table is generated by taking powers of 3 (multiplying by x). Log table is the reverse mapped exp table. InitGF initializes the exp table using GFMpyA (multiply by alpha == multiply by x). Showing the math starting at integer 27 = 1 x^3 * x, showing the long hand division of

ex = e0 * x modulo polynomial

                    1                  q = 1 = quotient
          -----------
1 1 1 2 2 | 1 0 0 0 0                  poly | ex
            1 1 1 2 2                  poly * q
            ---------
              2 2 1 1                  remainder

                    2                  q = 2 = quotient
          -----------
1 1 1 2 2 | 2 2 1 1 0                  poly | ex
            2 2 2 1 1                  poly * 2
            ---------
              0 2 0 2                  remainder

Basic math code with initialization:

typedef unsigned char BYTE;

/* GFS(3) */
#define GFS 3
/* GF(3^2) */
#define GF 81
/* alpha = 1x + 0 */
#define ALPHA 3

typedef struct{                         /* element of field */
    int     d;                          /* = dx^3 + cx^2 + bx + a */
    int     c;
    int     b;
    int     a;
}ELEM;

typedef struct{                         /* extended element of field */
    int     e;                          /* = ex^4 + dx^3 + cx^2 +bx + a */
    int     d;
    int     c;
    int     b;
    int     a;
}ELEMX;

/*----------------------------------------------------------------------*/
/*      GFAdd(i0, i1)                                                   */
/*----------------------------------------------------------------------*/
static int GFAdd(int i0, int i1)
{
ELEM e0, e1;
    e0 = aiI2E[i0];
    e1 = aiI2E[i1];
    e0.d = (e0.d + e1.d);
    if(e0.d >= GFS)e0.d -= GFS;
    e0.c = (e0.c + e1.c);
    if(e0.c >= GFS)e0.c -= GFS;
    e0.b = (e0.b + e1.b);
    if(e0.b >= GFS)e0.b -= GFS;
    e0.a = (e0.a + e1.a);
    if(e0.a >= GFS)e0.a -= GFS;
    return (((((e0.d*GFS)+e0.c)*GFS)+e0.b)*GFS)+e0.a;
}

/*----------------------------------------------------------------------*/
/*      GFSub(i0, i1)                                                   */
/*----------------------------------------------------------------------*/
static int GFSub(int i0, int i1)
{
ELEM e0, e1;
    e0 = aiI2E[i0];
    e1 = aiI2E[i1];
    e0.d = (e0.d - e1.d);
    if(e0.d < 0)e0.d += GFS;
    e0.c = (e0.c - e1.c);
    if(e0.c < 0)e0.c += GFS;
    e0.b = (e0.b - e1.b);
    if(e0.b < 0)e0.b += GFS;
    e0.a = (e0.a - e1.a);
    if(e0.a < 0)e0.a += GFS;
    return (((((e0.d*GFS)+e0.c)*GFS)+e0.b)*GFS)+e0.a;
}

/*----------------------------------------------------------------------*/
/*      GFMpy(i0, i1)           i0*i1       using logs                  */
/*----------------------------------------------------------------------*/
static int GFMpy(int i0, int i1)
{
    if(i0 == 0 || i1 == 0)
        return(0);
    return(aiExp[aiLog[i0]+aiLog[i1]]);
}

/*----------------------------------------------------------------------*/
/*      GFDiv(i0, i1)           i0/i1                                   */
/*----------------------------------------------------------------------*/
static int GFDiv(int i0, int i1)
{
    if(i0 == 0)
        return(0);
    return(aiExp[(GF-1)+aiLog[i0]-aiLog[i1]]);
}


/*----------------------------------------------------------------------*/
/*      GFPow(i0, i1)           i0^i1                                   */
/*----------------------------------------------------------------------*/
static int GFPow(int i0, int i1)
{
    if(i1 == 0)
        return (1);
    if(i0 == 0)
        return (0);
    return(aiExp[(aiLog[i0]*i1)%(GF-1)]);
}

/*----------------------------------------------------------------------*/
/*      GFMpyA(i0)              i0*ALPHA    using low level math        */
/*----------------------------------------------------------------------*/
/* hard coded for elements of size 4 */
static int GFMpyA(int i0)
{
ELEM e0;
ELEMX ex;
int q;                                  /* quotient */
    e0 = aiI2E[i0];                     /* e0 = i0 split up */
    ex.e = e0.d;                        /* ex = e0*x */
    ex.d = e0.c;
    ex.c = e0.b;
    ex.b = e0.a;
    ex.a = 0;
    q = ex.e;
/*  ex.e -= q * pGFPoly.aata[0] % GFS;  ** always == 0 */
/*  if(ex.e < 0)ex.d += GFS;            ** always == 0 */
    ex.d -= q * pGFPoly.data[1] % GFS;
    if(ex.d < 0)ex.d += GFS;
    ex.c -= q * pGFPoly.data[2] % GFS;
    if(ex.c < 0)ex.c += GFS;
    ex.b -= q * pGFPoly.data[3] % GFS;
    if(ex.b < 0)ex.b += GFS;
    ex.a -= q * pGFPoly.data[4] % GFS;
    if(ex.a < 0)ex.a += GFS;
    return (((((ex.d*GFS)+ex.c)*GFS)+ex.b)*GFS)+ex.a;
}

/*----------------------------------------------------------------------*/
/*      InitGF  Initialize Galios Stuff                                 */
/*----------------------------------------------------------------------*/
static void InitGF(void)
{
int i;
int t;

    for(i = 0; i < GF; i++){    /* init index to element table */
        t = i;
        aiI2E[i].a = t%GFS;
        t /= GFS;
        aiI2E[i].b = t%GFS;
        t /= GFS;
        aiI2E[i].c = t%GFS;
        t /= GFS;
        aiI2E[i].d = t;
    }

    pGFPoly.size = 5;           /* init GF() polynomial */
    pGFPoly.data[0] = 1;
    pGFPoly.data[1] = 1;
    pGFPoly.data[2] = 1;
    pGFPoly.data[3] = 2;
    pGFPoly.data[4] = 2;

    t = 1;                      /* init aiExp[] */
    for(i = 0; i < GF*2; i++){
        aiExp[i] = t;
        t = GFMpyA(t);
    }

    aiLog[0] = -1;              /* init aiLog[] */
    for(i = 0; i < GF-1; i++)
        aiLog[aiExp[i]] = i;
}


/*----------------------------------------------------------------------*/
/*      main                                                            */
/*----------------------------------------------------------------------*/
int main()
{
    InitGF();
    return(0);
}

Code to display a list of primitive polynomials for GF(3^4)

    pGFPoly.size = 5;           /* display primitive polynomials */
    pGFPoly.data[0] = 1;
    pGFPoly.data[1] = 0;
    pGFPoly.data[2] = 0;
    pGFPoly.data[3] = 0;
    pGFPoly.data[4] = 1;
    while(1){
        i = 0;
        t = 1;
        do{
            i++;
            t = GFMpyA(t);}
        while(t != 1);
        if(i == (GF-1)){
            printf("pGFPoly:    ");
            ShowVector(&pGFPoly);}
        pGFPoly.data[4] += 1;
        if(pGFPoly.data[4] == GFS){
            pGFPoly.data[4]  = 1;
            pGFPoly.data[3] += 1;
            if(pGFPoly.data[3] == GFS){
                pGFPoly.data[3]  = 0;
                pGFPoly.data[2] += 1;
                if(pGFPoly.data[2] == GFS){
                    pGFPoly.data[2]  = 0;
                    pGFPoly.data[1] += 1;
                    if(pGFPoly.data[1] == GFS){
                        break;}}}}}

This produces this list:

1 0 0 1 2   The one normally used x^4 + x + 2
1 0 0 2 2
1 1 0 0 2
1 1 1 2 2   I used this to test all 5 terms
1 1 2 2 2
1 2 0 0 2
1 2 1 1 2
1 2 2 1 2
rcgldr
  • 27,407
  • 3
  • 36
  • 61
  • ok, thanks for providing `GFAdd`, `GFMpy`, `GFDiv`. So you say that there is no better way than to implement the GF arithmetics. For a concrete p^k you can get this list by pasting `for(i=3^4,3^4*2-1, #divisors(Pol(t=digits(i,3))*Mod(1,3))==2 && print(t))` into http://pari.math.u-bordeaux.fr/gp.html if you don't have PARI/GP installed at home. Also: One may prefer the Conway polynomials to ensure compatibility between extension fields of a prime number field. That one would be `f[3,4] = X^4 + 2 X^3 + 2` according to https://www.math.rwth-aachen.de/~Frank.Luebeck/data/ConwayPol/CP3.html – Max Jun 15 '22 at 16:33
  • I didn't mean to say the list isn't interesting or incomplete. I stuffed multiple comments in one, maybe a bad idea. Yes, the Conway polynomial f[3,4] is indeed in the list, but since you put a comment "usually chosen" after (10012) I thought it may be worth while mentioning also (12002) which is another, maybe more canonical choice. – Max Jun 16 '22 at 13:48
  • As to the size of q, it is a priori arbitrarily large. I actually wanted to write a better implementation of `scipy.linalg.hadamard` which currently works only for *n* = 2^k , but for many other arguments *n* one can construct a Hadamard matrix of size *n* using the Jacobsthal matrix of some smaller size *q* which depends on the form of *n* in a nontrivial way, see https://en.wikipedia.org/wiki/Paley_construction – Max Jun 16 '22 at 13:56
  • @Max - 10012 has one less multiply versus 12002, but GF(3^4) isn't common. For n = 2^k, carryless multiply such as PCLMULQDQ which multiplies two 64 bit operands to produce a 128 bit product (the most significant bit will always be zero, so this could be considered a 127 bit product). Dividing by a constant can be implemented using a carryless multiply by inverse. Other than GF(929), used for PDF 417 bar codes, what math uses GF(p^n), where p is a prime number other than 2? – rcgldr Jun 16 '22 at 15:34
  • yes, true, I know that x^n+ax+b is the most efficient choice. But the Conway polynomials have the advantage that you can see GF(p^k) with k=1,...,n-1 as subset of GF(p^n) without risk of ambiguities. – Max Jun 16 '22 at 16:04
  • Once again, I need the quadratic character in GF(p^n) with p > 2 to construct Hadamard matrices of size other than 2^k through the Paley construction. – Max Jun 16 '22 at 16:06
  • @Max - I haven't worked with Hadamard matrices or Conway polynomials. Looking at them now. Most of my work with finite fields has been with error correction code, where choosing a polynomial is a one time event for a specific field. Sometimes a field like GF(2^8) is mapped to a composite field like GF(((2^2)^2)^2) to reduce gate count to reduce hardware gate count for inversion (1/x), but this is small enough that a brute force search is done to find a mapping that results in minimal gate count. – rcgldr Jun 16 '22 at 21:35
  • @Max - somewhat similar to Conway polynomials, if mapping GF(2^n) with reducing primitive polynomial P(x) with 1 bit coefficients to GF((2^(n/k))^k): interpret P(x), to be P'(x), with 0 and 1 coefficients in GF(2^(n/k)) with primitive reducing polynomial Q(x). There are n/k primitive polynomial factors of degree k for P'(x), any of which can be used to map from GF(2^n) to GF((2^(n/k))^k) . However, for n >= 64, finding a mapping would take too long, and in some cases the math is implemented in GF((2^(n/k)^k), knowing it could be mapped to/from some GF(2^n), but without ever actually mapping. – rcgldr Jun 21 '22 at 00:06
  • Sorry, not sure whether I got this correctly. Is P' the derivative of P? What would be GF((2^(n/k))^k) other than GF(2^n) ? Did you mean GF(2^(n/k))^k ? (GF(q) only exists for q=p^m with prime p.) Also, I think one would recursively / incrementally fix one choice for every k = 2, 3, 4, ...,n and then get the (maybe non canonical) mappings by going from n to the desired k by composing the mappings from G(2^m) -> G(2^(m-1)) with m=n, n-1, ..., k+1. – Max Jun 21 '22 at 14:42
  • @Max - P`(x) is a an interpretation of P(x), where the 1 bit coefficients of P(x) are interpreted to be elements of GF((2^(n/k)^k). I posted an example of mapping from GF(2^32) to GF((2^16)^2), in [this question](https://math.stackexchange.com/questions/3754201). – rcgldr Jun 21 '22 at 17:57
  • @Max - An alternative to this approach is to choose a primitive polynomial for the composite field (usually one that's optimal to implement in hardware), and then do a brute force search for any of the `k` primitive elements out of totient(q-1) primitive elements of GF(q) where the mapping works. I posted an example of mapping from GF(2^8) to GF(((2^2)^2)^2) in [this answer](https://math.stackexchange.com/questions/3739707/3756361#3756361) . – rcgldr Jun 21 '22 at 18:04
  • Thanks... I think there is still a confusion between ((GF(2)²)²)² as the OP writes "correctly", and GF(((2²)²)²) as you write in you answer (in the current version, I guess it may be corrected soon) , but actually you seem to use coefficients in GF(2)^k and not in GF(2^k) in spite of what you write. I'll comment there. – Max Jun 22 '22 at 23:50