Use of LazyLaurentSeriesRing.

I would like to better understand the use of ‘LazyLaurentSeriesRing’ in Sage.

The expansion of the function ‘Omega’, as defined below, sometimes works, sometimes not.

More precisely, it works in the cases m = 0, 1, 2, 4, and does not work for m = 3, 5, 6.

What’s the trick?

def Omega(m, z) :
    if m == 0: return z**0
    if m == 1: return exp(z)
    if m == 2: return cosh(z)
    if m == 3: return (exp(z) + 2 * exp(-z / 2) * cos(z * 3 ** (1 / 2) / 2)) / 3
    if m == 4: return (cosh(z) + cos(z)) / 2
    if m == 5: return (exp(z)
            + 2 * exp(-cos(pi / 5) * z) * cos(sin(pi / 5) * z)
            + 2 * exp(cos((2 * pi) / 5) * z) * cos(sin((2 * pi) / 5) * z) ) / 5
    if m == 6: return (cosh(z) + 2 * cos(z * sqrt(3) / 2) * cosh(z / 2)) / 3
    return 0

Test:

L = LazyLaurentSeriesRing(QQ, 'z')
for m in [0, 1, 2, 4]:
    f = Omega(m, L.gen())
    print(f"m={m}: {[factorial(n)*f.coefficient(n) for n in range(24)]}")

The solution seems to be to shift the problem to complex numbers and then use QQbar.

def Omega(m, lng) -> list[int]:
    L = LazyLaurentSeriesRing(QQbar, 'x')
    x = L.gen()
    if m == 0: return [L(0^n) for n in (0..lng)]
    w = QQbar(exp(2 * pi * I / m))
    f = sum(exp(x * w**j) for j in range(m)) / m 
    return [round(real(factorial(n)*f.coefficient(n))) for n in (0..lng)]

for m in (0..6): print(Omega(m, 24))

Instead of round(real(factorial(n)*f.coefficient(n))) you can use simply ZZ(factorial(n)*f.coefficient(n)) (provided that you know that the result is integer).

The problem is that you are mixing up symbolic values, like 3 ** (1 / 2), cos(pi / 5), etc., that do not exist in QQ and L.

A possible solution is to extend the base ring from QQ to AA to include radicals like sqrt(3), sqrt(5). However, explicit conversion of those expression into AA is needed:

def Omega(m, z) :
    if m == 0: return z**0
    if m == 1: return exp(z)
    if m == 2: return cosh(z)
    if m == 3: return (exp(z) + 2 * exp(-z / 2) * cos(z * sqrt(AA(3)) / 2)) / 3
    if m == 4: return (cosh(z) + cos(z)) / 2
    if m == 5: return (exp(z)
            + 2 * exp(-AA(cos(pi / 5)) * z) * cos(AA(sin(pi / 5)) * z)
            + 2 * exp(AA(cos((2 * pi) / 5)) * z) * cos(AA(sin((2 * pi) / 5)) * z) ) / 5
    if m == 6: return (cosh(z) + 2 * cos(z * sqrt(AA(3)) / 2) * cosh(z / 2)) / 3
    return 0

L = LazyLaurentSeriesRing(AA, 'z')
for m in (0..6):
    f = Omega(m, L.gen())
    print(f"m={m}: {[factorial(n)*f.coefficient(n).radical_expression() for n in range(24)]}")

Excellent! Thank you so much!

However, (1) the execution time is considerable, and (2) I would like to write a general function that doesn’t require any special pre-processing of the input function. Perhaps ‘LazyLaurentSeriesRing’ simply isn’t the right ring for this. Are there any better solutions?

To address (1), you may consider working in a smaller field such as K.<r3,r5> = NumberField([x^2-3,x^2-5]) that contains all your constants. I do not quite follow your question (2) - what do you call “pre-processing”? Again, the issue was that if you define your series over the rational field QQ, all coefficients must come from that field, and so you cannot have symbolic constants like sqrt(3). If you want to work with those, you need to define your series over a larger field and embed all constants into it.

Btw, as an alternative to exact fields, you may consider defining L over the symbolic ring SR (just change QQ to SR in your original code), although I’d expect even worse running time here.