Ask Your Question
1

How to handle a root of unity with symbolic exponents [closed]

asked 2025-05-29 19:38:14 +0200

jarch2 gravatar image

updated 2025-06-01 20:08:33 +0200

I'm doing some calculations involving matrices whose entries look something like q^L, where q is a root of unity and L is meant to be a generic complex number that I would prefer to leave as a symbol. I would actually prefer to have the final result be displayed in terms of q as well, but that isn't as big of a sticking point.

For small dimensions, if I work over the symbolic ring (SR) and leave q as a symbol too, everything behaves as it should. The problem is when moving to higher dimensions, it not only gets prohibitively slow, but there are a few entries that I know (by other means) should simplify to zero but aren't, even if I replace the symbol q with it's concrete complex value.

I was recommended Cyclotomic Fields as a way to help Sage deal with fixed roots of unity more efficiently, but I can't figure out the right way (if there is one) to work over a Cyclotomic Field where the exponents can include symbols without just putting everything back into SR which defeats the purpose.

Code leaving everything in SR since that worked okay. The choice to use simplify_rational() followed by simplify_expand() was just because simplify_full() was a lot slower without better results.

r = 2

var('q L')

base_field = SR

recip_diff = q^(1/2) - q^(-1/2)

def quantum(num):
    return (q^(num/2) - q^(-num/2)) / recip_diff

def quantum_factorial(num):
    if num <= 1:
        return 1
    return quantum(num) * quantum_factorial(num-1)

def qexp(x,r):
    ans = matrix(r^2,r^2,0)
    for n in range(r):
        ans += (q^(n*(n-1)/4) / quantum_factorial(n)) * x^n
    return ans

E = matrix(base_field, r,r,0)
F = matrix(base_field, r,r,0)
K = matrix(base_field, r,r,0)

for i in range(r):
    if i < r-1:
        E[i,i+1] = quantum(r-(i+1)+L)
    if i > 0:
        F[i,i-1] = quantum(i)
    K[i,i] = q^((r-2*i-1+L)/2)

H = matrix(base_field, r^2, r^2, 0)
P = matrix(base_field, r^2, r^2, 0)
for i in range(r):
    for j in range(r):
        k = r*i + j
        H[k,k] = q^((r-2*i-1+L)*(r-2*j-1+L)/4)
        P[k,r*j + i] = 1


R = P * H * qexp(recip_diff * E.tensor_product(F),r)
R = R.simplify_rational().expand()

crossE = E.tensor_product(K) + matrix.identity(r).tensor_product(E)
crossF = F.tensor_product(matrix.identity(r)) + K.inverse().tensor_product(F)
crossK = K.tensor_product(K)

crossE = crossE.simplify_rational().expand()
crossF = crossF.simplify_rational().expand()
crossK = crossK.simplify_rational().expand()

kernel = crossE.right_kernel()
eigen_vectors = crossK.eigenvectors_right()
new_basis = []
for i in range(r):
    v = kernel.intersection(span(eigen_vectors[i][1], kernel.base_ring())).basis()[0]
    new_basis.append(v)
    for j in range(r - 1):
        new_basis.append(crossF * new_basis[-1])

new_basis = matrix(new_basis).transpose().simplify_rational().expand()
inv_basis = new_basis.inverse().simplify_rational().expand()

newR = inv_basis * R * new_basis
edit retag flag offensive reopen merge delete

Closed for the following reason the question is answered, right answer was accepted by jarch2
close date 2025-06-02 06:09:38.313521

Comments

In the code you seem to raise q only to half-integer powers. If that's the only kind of non-integer powers, then you can define another root of unity, say w of the degree twice as much as that of q to represent the square root of q, and replace q with w^2 to stick to integer powers.

Btw, what is the degree of q in your code? Also, L is not defined.

Max Alekseyev gravatar imageMax Alekseyev ( 2025-06-01 21:23:07 +0200 )edit

It actually goes up to quarter integers, but replacing q with a higher degree root of unity to get only integer exponents didn't really change much in terms of speed or simplification effectiveness. If it helps, the degree of q is r in the code.

L being left as a symbolic parameter is intentional, since these calculations are going towards a knot invariant that is a function in L, i.e. each knot can be assigned a function in L such that equivalent knots always get the same function regardless of different presentations. Plugging in concrete values of L isn't really an option for my purposes unfortunately, since I'm trying to investigate some properties of this invariant by looking at some of the calculations that go into its construction. (ADO invariants for the curious)

jarch2 gravatar imagejarch2 ( 2025-06-01 22:03:05 +0200 )edit

Having integer exponents will allow you to work in cyclotomic field, which is very efficient and will eliminate the need for slow simplify/expand/etc. symbolic functions.

Max Alekseyev gravatar imageMax Alekseyev ( 2025-06-02 00:49:16 +0200 )edit

As for L, you can introduce a polynomial variable denoting w^L and another one for w^(L^2), which will allow you eliminate symbolics from the exponents of w.

Max Alekseyev gravatar imageMax Alekseyev ( 2025-06-02 00:52:55 +0200 )edit

1 Answer

Sort by ยป oldest newest most voted
1

answered 2025-06-02 01:20:40 +0200

Max Alekseyev gravatar image

Here is my take to switch your code to work over cyclotomic field as I explained in the comments:

r = 2

FF.<w> = CyclotomicField(4*r)           # we have q = w^4
KK.<L1,L2> = LaurentPolynomialRing(FF)          # L1 stands for w^L, L2 stands for w^(L^2)

base_field = KK

recip_diff = w^2 - w^(-2)     #  = q^(1/2) - q^(-1/2)

def quantum(num,Lcoef=0):
    return (w^(2*num)*L1^Lcoef - w^(-2*num)*L1^(-Lcoef)) // recip_diff

def quantum_factorial(num):
    if num <= 1:
        return 1
    return quantum(num) * quantum_factorial(num-1)

def qexp(x,r):
    ans = matrix(r^2,r^2,0)
    for n in range(r):
        ans += (w^(n*(n-1)) / quantum_factorial(n)) * x^n
    return ans

E = matrix(base_field, r,r,0)
F = matrix(base_field, r,r,0)
K = matrix(base_field, r,r,0)

for i in range(r):
    if i < r-1:
        E[i,i+1] = quantum(r-(i+1),1)
    if i > 0:
        F[i,i-1] = quantum(i)
    K[i,i] = w^((r-2*i-1)*2) * L1^2

H = matrix(base_field, r^2, r^2, 0)
P = matrix(base_field, r^2, r^2, 0)
for i in range(r):
    for j in range(r):
        k = r*i + j
        H[k,k] = w^((r-2*i-1)*(r-2*j-1)) * L1^(r-2*i-1 + r-2*j-1) * L2                          # = q^((r-2*i-1+L)*(r-2*j-1+L)/4)
        P[k,r*j + i] = 1


R = P * H * qexp(recip_diff * E.tensor_product(F),r)

crossE = E.tensor_product(K) + matrix.identity(r).tensor_product(E)
crossF = F.tensor_product(matrix.identity(r)) + K.inverse().change_ring(KK).tensor_product(F)
crossK = K.tensor_product(K)

# linear algebra does not work over our ring KK, so we temporarily switch to SR
kernel = crossE.change_ring(SR).right_kernel()
eigen_vectors = crossK.change_ring(SR).eigenvectors_right()
new_basis = []
for i in range(r):
    v = kernel.intersection(span(eigen_vectors[i][1], kernel.base_ring())).basis()[0]
    new_basis.append(v)
    for j in range(r - 1):
        new_basis.append(crossF * new_basis[-1])

new_basis = matrix(KK,new_basis).transpose()     # return back to KK
inv_basis = new_basis.inverse()

newR = inv_basis * R * new_basis
edit flag offensive delete link more

Comments

YOU ARE AWESOME! I had to tweak a couple things to make it run properly, but introducing L1 and L2 as their own symbols is exactly what I needed to do, thank you so much! Everything is beautiful and soooooo fast :)

(Just in case you're curious, there was a small error of a factor of two in the power of L1 in quantum() and also, for r > 2 I had to switch over to the fraction field of a two variable polynomial ring instead of the Laurent polynomials. I also decided to construct the eigenspace for crossK directly, since it follows a particular pattern wrt the standard basis.)

jarch2 gravatar imagejarch2 ( 2025-06-02 05:03:24 +0200 )edit

You are welcome. Good luck with your computations!

Max Alekseyev gravatar imageMax Alekseyev ( 2025-06-02 05:15:53 +0200 )edit

Question Tools

Stats

Asked: 2025-05-29 19:29:40 +0200

Seen: 65 times

Last updated: Jun 02