Skip to content

Quickstart

This quickstart demonstrates how to use polpack to evaluate polynomial families, combinatorial sequences, and special functions. For mathematical background, see the Theory section.


Common Interface

Most polpack routines follow a consistent pattern: you pre-allocate a NumPy array, pass it to the routine along with parameters, and the routine fills it in place.

import numpy as np
import polpack

# Pattern: pre-allocate output array, then call routine
n = 5  # highest degree
x = 0.5  # evaluation point
cx = np.zeros(n + 1, dtype=np.float64, order="F")  # (1)

polpack.cheby_t_poly(1, n, x, cx)
# cx[k] now contains T_k(x) for k = 0, 1, ..., n

Array order

All output arrays must be pre-allocated. Use order="F" (Fortran-contiguous) for consistency with the underlying Fortran core. Integer routines typically use dtype=np.int32; floating-point routines use dtype=np.float64.


Orthogonal Polynomials

Chebyshev Polynomials (First Kind)

Chebyshev polynomials \(T_n(x)\) are optimal for function approximation — they minimize the maximum absolute error among all degree-\(n\) polynomials with leading coefficient 1.

import numpy as np
import polpack

m = 1  # number of evaluation points
n = 6  # highest degree
x = 0.5  # evaluation point

cx = np.zeros(n + 1, dtype=np.float64, order="F")
polpack.cheby_t_poly(m, n, x, cx)

print(f"Chebyshev T_k({x}) for k = 0 ... {n}:")
for k, val in enumerate(cx):
    print(f"  T_{k}({x}) = {val:.6f}")

To get the polynomial coefficients or zeros of \(T_n\):

import numpy as np
import polpack

n = 4
c = np.zeros((n + 1, n + 1), dtype=np.float64, order="F")
polpack.cheby_t_poly_coef(n, c)
print(f"Coefficients of T_{n}(x) (lowest to highest power):\n{c[n, :]}")

z = np.zeros(n, dtype=np.float64, order="F")
polpack.cheby_t_poly_zero(n, z)
print(f"Zeros of T_{n}(x): {z}")

Legendre Polynomials

Legendre polynomials \(P_n(x)\) are orthogonal on \([-1, 1]\) with unit weight. They arise in potential theory, Gauss-Legendre quadrature, and spherical harmonics.

import numpy as np
import polpack

n = 10
x = 0.5
p = np.zeros(n + 1, dtype=np.float64, order="F")
dp = np.zeros(n + 1, dtype=np.float64, order="F")  # first derivatives

polpack.legendre_poly(n, x, p, dp)

print(f"P_n({x}) for n = 0 ... {n}:")
for k in range(n + 1):
    print(f"  P_{k}({x}) = {p[k]:.6f},  P'_{k}({x}) = {dp[k]:.6f}")

Hermite Polynomials (Physicist's)

Hermite polynomials \(H_n(x)\) arise in quantum mechanics (harmonic oscillator) and probability (Gaussian integration).

import numpy as np
import polpack

n = 5
x = 1.0
cx = np.zeros(n + 1, dtype=np.float64, order="F")

polpack.hermite_poly_phys(n, x, cx)

print(f"H_n({x}) for n = 0 ... {n}:")
for k, val in enumerate(cx):
    print(f"  H_{k}({x}) = {val:.4f}")

Jacobi Polynomials

Jacobi polynomials \(P_n^{(\alpha,\beta)}(x)\) generalize both Legendre (\(\alpha=\beta=0\)) and Chebyshev polynomials. They are orthogonal on \([-1, 1]\) with weight \((1-x)^\alpha (1+x)^\beta\).

import numpy as np
import polpack

n = 5
alpha, beta = 0.5, 0.5  # reduces to Gegenbauer with parameter 1.0
x = 0.3
cx = np.zeros(n + 1, dtype=np.float64, order="F")

polpack.jacobi_poly(n, alpha, beta, x, cx)

print(f"P_k^({alpha},{beta})({x}) for k = 0 ... {n}:")
for k, val in enumerate(cx):
    print(f"  P_{k}({x}) = {val:.6f}")

Laguerre Polynomials

Laguerre polynomials \(L_n(x)\) are orthogonal on \([0, \infty)\) with weight \(e^{-x}\). They appear in the radial wavefunctions of the hydrogen atom.

import numpy as np
import polpack

n = 6
x = 1.5
cx = np.zeros(n + 1, dtype=np.float64, order="F")

polpack.laguerre_poly(n, x, cx)

print(f"L_n({x}) for n = 0 ... {n}:")
for k, val in enumerate(cx):
    print(f"  L_{k}({x}) = {val:.6f}")

Gegenbauer (Ultraspherical) Polynomials

Gegenbauer polynomials \(C_n^{(\alpha)}(x)\) generalize both Legendre (\(\alpha=1/2\)) and Chebyshev (\(\alpha=1\)) polynomials.

import numpy as np
import polpack

n = 4
alpha = 1.0  # reduces to Chebyshev U when alpha = 1
x = 0.5
cx = np.zeros(n + 1, dtype=np.float64, order="F")

polpack.gegenbauer_poly(n, alpha, x, cx)
print(f"C_k^({alpha})({x}) for k = 0 ... {n}: {cx}")

Bernstein Polynomials

Bernstein basis polynomials form the foundation of Bézier curves. All \(n+1\) basis functions of degree \(n\) are evaluated in a single call.

import numpy as np
import polpack

n = 3  # degree
x = 0.4  # point in [0, 1]
bern = np.zeros(n + 1, dtype=np.float64, order="F")

polpack.bernstein_poly(n, x, bern)

print(f"Bernstein basis B_{{k,{n}}}({x}) for k = 0 ... {n}:")
for k, val in enumerate(bern):
    print(f"  B_{k},{n}({x}) = {val:.6f}")

# Verify partition of unity: sum should be 1.0
print(f"Sum = {bern.sum():.6f}")

For evaluation on a general interval \([a, b]\):

import numpy as np
import polpack

n = 3
x, a, b = 2.0, 1.0, 4.0
bern = np.zeros(n + 1, dtype=np.float64, order="F")

polpack.bpab(n, x, a, b, bern)
print(f"Bernstein basis on [{a}, {b}] at x={x}: {bern}")

Combinatorial Sequences

Bell Numbers

The Bell number \(B_n\) counts the number of ways to partition a set of \(n\) elements.

import numpy as np
import polpack

n = 14
b = np.zeros(n + 1, dtype=np.int32, order="F")
polpack.bell(n, b)

print(f"Bell numbers B_0 ... B_{n}:")
for k, val in enumerate(b):
    print(f"  B_{k} = {val}")

Catalan Numbers

The Catalan number \(C_n\) counts binary trees, valid parenthesizations, and many other combinatorial structures.

import numpy as np
import polpack

n = 10
c = np.zeros(n + 1, dtype=np.int32, order="F")
polpack.catalan(n, c)
print(f"Catalan numbers C_0 ... C_{n}: {c}")

Stirling Numbers

Stirling numbers of the first kind \(s(n, k)\) count permutations of \(n\) objects with exactly \(k\) cycles. Stirling numbers of the second kind \(S(n, k)\) count the number of ways to partition \(n\) objects into \(k\) non-empty subsets.

import numpy as np
import polpack

n, m = 6, 6
s1 = np.zeros((n, m), dtype=np.int32, order="F")
s2 = np.zeros((n, m), dtype=np.int32, order="F")

polpack.stirling1(n, m, s1)
polpack.stirling2(n, m, s2)

print("Stirling numbers of the first kind s(n,k):")
print(s1)
print("\nStirling numbers of the second kind S(n,k):")
print(s2)

Fibonacci Numbers

import numpy as np
import polpack

# Recursive: compute F_1 ... F_n
n = 15
f = np.zeros(n, dtype=np.int32, order="F")
polpack.fibonacci_recursive(n, f)
print(f"Fibonacci F_1 ... F_{n}: {f}")

# Zeckendorf decomposition: represent N as sum of non-consecutive Fibonacci numbers
n_val = 100
m_max = 20
m = np.zeros(1, dtype=np.int32, order="F")
il = np.zeros(m_max, dtype=np.int32, order="F")
fl = np.zeros(m_max, dtype=np.int32, order="F")
polpack.zeckendorf(n_val, m_max, m, il, fl)
terms = fl[: m[0]]
print(f"{n_val} = {' + '.join(str(v) for v in terms)}")

Delannoy and Motzkin Numbers

import numpy as np
import polpack

# Delannoy numbers: paths from (0,0) to (m,n) using steps (1,0), (0,1), (1,1)
m, n = 5, 5
a = np.zeros((m + 1, n + 1), dtype=np.int32, order="F")
polpack.delannoy(m, n, a)
print(f"Delannoy A(m,n):\n{a}")

# Motzkin numbers
n = 10
mo = np.zeros(n + 1, dtype=np.int32, order="F")
polpack.motzkin(n, mo)
print(f"Motzkin numbers M_0 ... M_{n}: {mo}")

Number Theory

Euler Totient and Divisor Functions

import numpy as np
import polpack

# Euler totient: phi(n) = number of integers in [1,n] coprime to n
n = 12
phin = np.zeros(1, dtype=np.int32, order="F")
polpack.phi(n, phin)
print(f"phi({n}) = {phin[0]}")

# Sum of divisors sigma(n)
sigma_n = np.zeros(1, dtype=np.int32, order="F")
polpack.sigma(n, sigma_n)
print(f"sigma({n}) = {sigma_n[0]}")

# Number of divisors tau(n)
taun = np.zeros(1, dtype=np.int32, order="F")
polpack.tau(n, taun)
print(f"tau({n}) = {taun[0]}")

Prime Numbers and Factorization

import numpy as np
import polpack

# n-th prime (up to n=1600, largest prime stored is 13499)
p10 = polpack.prime(10)
print(f"The 10th prime is {p10}")

# Primality test
for candidate in [17, 18, 97, 100]:
    result = polpack.i4_is_prime(candidate)
    print(f"i4_is_prime({candidate}) = {result}")

# Prime factorization: n = product(factor[i]^power[i]) * nleft
n = 360
factor_max = 20
factor_num = np.zeros(1, dtype=np.int32, order="F")
factor = np.zeros(factor_max, dtype=np.int32, order="F")
power = np.zeros(factor_max, dtype=np.int32, order="F")
nleft = np.zeros(1, dtype=np.int32, order="F")
polpack.i4_factor(n, factor_max, factor_num, factor, power, nleft)
nf = factor_num[0]
parts = " * ".join(f"{factor[i]}^{power[i]}" for i in range(nf))
print(f"{n} = {parts}")

Moebius and Mertens Functions

import numpy as np
import polpack

for n in [1, 6, 12, 13]:
    mu = np.zeros(1, dtype=np.int32, order="F")
    polpack.moebius(n, mu)
    m = polpack.mertens(n)
    print(f"mu({n}) = {mu[0]},  M({n}) = {m}")

Special Functions

Riemann Zeta Function

import polpack
import math

for p in [2.0, 3.0, 4.0]:
    z = polpack.zeta(p)
    print(f"zeta({p}) = {z:.8f}")

# Verification: zeta(2) = pi^2/6
print(f"pi^2/6     = {math.pi**2 / 6:.8f}")

Lambert W Function

The Lambert W function satisfies \(W(x) \cdot e^{W(x)} = x\).

import polpack

for x in [1.0, math.e, 10.0]:
    w = polpack.lambert_w(x)
    print(f"W({x:.4f}) = {w:.8f}  [check: W*exp(W) = {w * math.exp(w):.8f}]")

Error Function

import polpack

for x in [0.0, 0.5, 1.0, 2.0]:
    e = polpack.r8_erf(x)
    print(f"erf({x}) = {e:.8f}")

Bernoulli Numbers

import numpy as np
import polpack

n = 12
b = np.zeros(n + 1, dtype=np.float64, order="F")
polpack.bernoulli_number(n, b)

print(f"Bernoulli numbers B_0 ... B_{n}:")
for k, val in enumerate(b):
    if val != 0.0:
        print(f"  B_{k} = {val:.10f}")

Collatz Sequences

import polpack

for start in [6, 27, 100]:
    steps = polpack.collatz_count(start)
    print(f"Collatz({start}): {steps} steps to reach 1")

# Find the integer in [1, N] with the longest Collatz sequence
n = 1000
i_max = np.zeros(1, dtype=np.int32, order="F")
j_max = np.zeros(1, dtype=np.int32, order="F")
polpack.collatz_count_max(n, i_max, j_max)
print(f"In [1, {n}]: longest sequence starts at {i_max[0]} ({j_max[0]} steps)")

Spherical Harmonics

polpack can evaluate the real and imaginary parts of the spherical harmonic function \(Y_l^m(\theta, \phi)\):

import numpy as np
import polpack

l, m = 2, 1
theta = 0.5  # polar angle (radians)
phi = 1.0  # azimuthal angle (radians)

c = np.zeros(1, dtype=np.float64, order="F")  # real part
s = np.zeros(1, dtype=np.float64, order="F")  # imaginary part

polpack.spherical_harmonic(l, m, theta, phi, c, s)
print(f"Y_{l}^{m}(theta={theta}, phi={phi})")
print(f"  Real part      = {c[0]:.8f}")
print(f"  Imaginary part = {s[0]:.8f}")