def jacobi_der(n, alpha, beta, x): """First derivative of Pn with respect to x, at points x. Parameters ---------- n : int polynomial order alpha : float first weight parameter beta : float second weight parameter x : numpy.ndarray x coordinates to evaluate at Returns ------- numpy.ndarray jacobi polynomial evaluated at the given points """ # see https://dlmf.nist.gov/18.9 # dPn = (1/2) (n + a + b + 1)P_{n-1}^{a+1,b+1} # first two terms are specialized for speed if n == 0: return np.zeros_like(x) if n == 1: return np.ones_like(x) * (0.5 * (n + alpha + beta + 1)) Pn = jacobi(n - 1, alpha + 1, beta + 1, x) coef = 0.5 * (n + alpha + beta + 1) return coef * Pn
def hermite_H_der_sequence(ns, x): """First derivative of He_[ns] with respect to x, at points x. Parameters ---------- ns : iterable of int rising polynomial orders, assumed to be sorted x : numpy.ndarray point(s) to evaluate at. Scalars and arrays both work. Returns ------- generator of numpy.ndarray equivalent to array of shape (len(ns), len(x)) """ # this function includes all the optimizations in the hermite_He func, # but excludes the note comments. Read that first if you're looking for # clarity # see also: prysm.polynomials.jacobi.jacobi_sequence for the meta machinery # in use here ns = list(ns) min_i = 0 if ns[min_i] == 0: yield np.zeros_like(x) min_i += 1 if min_i == len(ns): return if ns[min_i] == 1: yield 2 * np.ones_like(x) min_i += 1 if min_i == len(ns): return x2 = 2 * x P1 = x2 P2 = 4 * (x * x) - 2 if ns[min_i] == 2: yield 4 * P1 min_i += 1 if min_i == len(ns): return Pnm2, Pnm1 = P1, P2 max_n = ns[-1] for nn in range(3, max_n + 1): Pn = x2 * Pnm1 - (2 * (nn - 1)) * Pnm2 if ns[min_i] == nn: yield 2 * nn * Pnm1 min_i += 1 Pnm2, Pnm1 = Pnm1, Pn
def hermite_He_sequence(ns, x): """Probabilist's Hermite polynomials He of orders ns at points x. Parameters ---------- ns : iterable of int rising polynomial orders, assumed to be sorted x : numpy.ndarray point(s) to evaluate at. Scalars and arrays both work. Returns ------- generator of numpy.ndarray equivalent to array of shape (len(ns), len(x)) """ # this function includes all the optimizations in the hermite_He func, # but excludes the note comments. Read that first if you're looking for # clarity # see also: prysm.polynomials.jacobi.jacobi_sequence for the meta machinery # in use here ns = list(ns) min_i = 0 if ns[min_i] == 0: yield np.ones_like(x) min_i += 1 if min_i == len(ns): return if ns[min_i] == 1: yield x min_i += 1 if min_i == len(ns): return P1 = x P2 = x * x - 1 if ns[min_i] == 2: yield P2 min_i += 1 if min_i == len(ns): return Pnm2, Pnm1 = P1, P2 max_n = ns[-1] for nn in range(3, max_n + 1): Pn = x * Pnm1 - (nn - 1) * Pnm2 Pnm2, Pnm1 = Pnm1, Pn if ns[min_i] == nn: yield Pn min_i += 1
def dickson1(n, alpha, x): """Dickson Polynomial of the first kind of order n. Parameters ---------- n : int polynomial order alpha : float shape parameter if alpha = -1, the dickson polynomials are Fibonacci Polynomials if alpha = 0, the dickson polynomials are the monomials x^n if alpha = 1, the dickson polynomials and cheby1 polynomials are related by D_n(2x) = 2T_n(x) x : numpy.ndarray coordinates to evaluate the polynomial at Returns ------- numpy.ndarray D_n(x) """ if n == 0: return np.ones_like(x) * 2 if n == 1: return x # general recursive polynomials: # P0, P1 are the n=0,1 seed terms # Pnm1 = P_{n-1}, Pnm2 = P_{n-2} P0 = np.ones_like(x) * 2 P1 = x Pnm2 = P0 Pnm1 = P1 for _ in range(2, n + 1): Pn = x * Pnm1 - alpha * Pnm2 Pnm1, Pnm2 = Pn, Pnm1 return Pn
def hermite_He(n, x): """Probabilist's Hermite polynomial He of order n at points x. Parameters ---------- n : int polynomial order x : numpy.ndarray point(s) to evaluate at. Scalars and arrays both work. Returns ------- numpy.ndarray He_n(x) """ # note: A, B, C = 1, 0, n # avoid a "recurrence_abc_He" function call on each loop to do these inline, # and avoiding adding zero to an array (waste work) if n == 0: return np.ones_like(x) if n == 1: return x # if n not in (0,1), then n >= 2 # standard three-term recurrence relation # Pn+1 = (An x + Bn) Pn - Cn Pn-1 # notation here does Pn = (An-1 ...) # Pnm2 = Pn-2 # Pnm1 = Pn-1 # i.e., starting from P2 = (A1 x + B1) P1 - C1 P0 # # given that, we optimize P2 significantly by seeing that: # P0 = 1 # A, B, C = 1, 0, n # P2 = x P1 - 1 # -> P2 == x^2 - 1 P2 = x * x - 1 if n == 2: return P2 Pnm2 = x Pnm1 = P2 for nn in range(3, n + 1): # it would look like this without optimization # Pn = (A * x + B) * Pnm1 - C * Pnm2 # A, B, C = 1, 0, n Pn = x * Pnm1 - (nn - 1) * Pnm2 Pnm2, Pnm1 = Pnm1, Pn return Pn
def hermite_H(n, x): """Physicist's Hermite polynomial H of order n at points x. Parameters ---------- n : int polynomial order x : numpy.ndarray point(s) to evaluate at. Scalars and arrays both work. Returns ------- numpy.ndarray H_n(x) """ if n == 0: return np.ones_like(x) if n == 1: return 2 * x # if n not in (0,1), then n >= 2 # standard three-term recurrence relation # Pn+1 = (An x + Bn) Pn - Cn Pn-1 # notation here does Pn = (An-1 ...) # Pnm2 = Pn-2 # Pnm1 = Pn-1 # i.e., starting from P2 = (A1 x + B1) P1 - C1 P0 # # given that, we optimize P2 significantly by seeing that: # P0 = 1 # A, B, C = 2, 0, 2n # P2 = x P1 - 1 # -> P2 == 4^2 - 2 # another optimization: Ax == 2x == P1, so we compute that just once x2 = 2 * x P2 = 4 * (x * x) - 2 if n == 2: return P2 Pnm2 = x2 Pnm1 = P2 for nn in range(3, n + 1): # it would look like this without optimization # Pn = (A * x + B) * Pnm1 - C * Pnm2 # A, B, C = 2, 0, 2n Pn = x2 * Pnm1 - (2 * (nn - 1)) * Pnm2 Pnm2, Pnm1 = Pnm1, Pn return Pn
def dickson2(n, alpha, x): """Dickson Polynomial of the second kind of order n. Parameters ---------- n : int polynomial order alpha : float shape parameter if alpha = -1, the dickson polynomials are Lucas Polynomials x : numpy.ndarray coordinates to evaluate the polynomial at Returns ------- numpy.ndarray E_n(x) """ if n == 0: return np.ones_like(x) if n == 1: return x # general recursive polynomials: # P0, P1 are the n=0,1 seed terms # Pnm1 = P_{n-1}, Pnm2 = P_{n-2} P0 = np.ones_like(x) P1 = x Pnm2 = P0 Pnm1 = P1 for _ in range(2, n + 1): Pn = x * Pnm1 - alpha * Pnm2 Pnm1, Pnm2 = Pn, Pnm1 return Pn
def dickson1_sequence(ns, alpha, x): """Sequence of Dickson Polynomial of the first kind of orders ns. Parameters ---------- ns : iterable of int rising polynomial orders, assumed to be sorted alpha : float shape parameter if alpha = -1, the dickson polynomials are Fibonacci Polynomials if alpha = 0, the dickson polynomials are the monomials x^n if alpha = 1, the dickson polynomials and cheby1 polynomials are related by D_n(2x) = 2T_n(x) x : numpy.ndarray coordinates to evaluate the polynomial at Returns ------- generator of numpy.ndarray equivalent to array of shape (len(ns), len(x)) """ ns = list(ns) min_i = 0 P0 = np.ones_like(x) * 2 if ns[min_i] == 0: yield P0 min_i += 1 if min_i == len(ns): return P1 = x if ns[min_i] == 1: yield P1 min_i += 1 if min_i == len(ns): return Pnm2 = P0 Pnm1 = P1 for i in range(2, ns[-1] + 1): Pn = x * Pnm1 - alpha * Pnm2 Pnm1, Pnm2 = Pn, Pnm1 if ns[min_i] == i: yield Pn min_i += 1
def dickson2_sequence(ns, alpha, x): """Sequence of Dickson Polynomial of the second kind of orders ns. Parameters ---------- ns : iterable of int rising polynomial orders, assumed to be sorted alpha : float shape parameter if alpha = -1, the dickson polynomials are Lucas Polynomials x : numpy.ndarray coordinates to evaluate the polynomial at Returns ------- numpy.ndarray D_n(x) """ ns = list(ns) min_i = 0 P0 = np.ones_like(x) if ns[min_i] == 0: yield P0 min_i += 1 if min_i == len(ns): return P1 = x if ns[min_i] == 1: yield P1 min_i += 1 if min_i == len(ns): return Pnm2 = P0 Pnm1 = P1 for i in range(2, ns[-1] + 1): Pn = x * Pnm1 - alpha * Pnm2 Pnm1, Pnm2 = Pn, Pnm1 if ns[min_i] == i: yield Pn min_i += 1
def jacobi(n, alpha, beta, x): """Jacobi polynomial of order n with weight parameters alpha and beta. Parameters ---------- n : int polynomial order alpha : float first weight parameter beta : float second weight parameter x : numpy.ndarray x coordinates to evaluate at Returns ------- numpy.ndarray jacobi polynomial evaluated at the given points """ if n == 0: return np.ones_like(x) elif n == 1: term1 = alpha + 1 term2 = alpha + beta + 2 term3 = (x - 1) / 2 return term1 + term2 * term3 Pnm1 = alpha + 1 + (alpha + beta + 2) * ((x - 1) / 2) A, B, C = recurrence_abc(1, alpha, beta) Pn = (A * x + B) * Pnm1 - C # no C * Pnm2 =because Pnm2 = 1 if n == 2: return Pn for i in range(3, n + 1): Pnm2, Pnm1 = Pnm1, Pn A, B, C = recurrence_abc(i - 1, alpha, beta) Pn = (A * x + B) * Pnm1 - C * Pnm2 return Pn
def Qbfs(n, x): """Qbfs polynomial of order n at point(s) x. Parameters ---------- n : int polynomial order x : numpy.array point(s) at which to evaluate Returns ------- numpy.ndarray Qbfs_n(x) """ # to compute the Qbfs polynomials, compute the auxiliary polynomial P_n # recursively. Simultaneously use the recurrence relation for Q_n # to compute the intermediary Q polynomials. # for input x, transform r = x ^ 2 # then compute P(r) and consequently Q(r) # and scale outputs by Qbfs = r*(1-r) * Q # the auxiliary polynomials are the jacobi polynomials with # alpha,beta = (-1/2,+1/2), # also known as the chebyshev polynomials of the third kind, V(x) # the first two Qbfs polynomials are # Q_bfs0 = x^2 - x^4 # Q_bfs1 = 1/19^.5 * (13 - 16 * x^2) * (x^2 - x^4) rho = x**2 # c_Q is the leading term used to convert Qm to Qbfs c_Q = rho * (1 - rho) if n == 0: return c_Q # == x^2 - x^4 if n == 1: return 1 / np.sqrt(19) * (13 - 16 * rho) * c_Q # c is the leading term of the recurrence relation for P c = 2 - 4 * rho # P0, P1 are the first two terms of the recurrence relation for auxiliary # polynomial P_n P0 = np.ones_like(x) * 2 P1 = 6 - 8 * rho Pnm2 = P0 Pnm1 = P1 # Q0, Q1 are the first two terms of the recurrence relation for Qm Q0 = np.ones_like(x) Q1 = 1 / np.sqrt(19) * (13 - 16 * rho) Qnm2 = Q0 Qnm1 = Q1 for nn in range(2, n + 1): Pn = c * Pnm1 - Pnm2 Pnm2 = Pnm1 Pnm1 = Pn g = g_qbfs(nn - 1) h = h_qbfs(nn - 2) f = f_qbfs(nn) Qn = (Pn - g * Qnm1 - h * Qnm2) * ( 1 / f) # small optimization; mul by 1/f instead of div by f Qnm2 = Qnm1 Qnm1 = Qn # Qn is certainly defined (flake8 can't tell the previous ifs bound the loop # to always happen once) return Qn * c_Q # NOQA
def Qbfs_sequence(ns, x): """Qbfs polynomials of orders ns at point(s) x. Parameters ---------- ns : Iterable of int polynomial orders x : numpy.array point(s) at which to evaluate Returns ------- generator of numpy.ndarray yielding one order of ns at a time """ # see the leading comment of Qbfs for some explanation of this code # and prysm:jacobi.py#jacobi_sequence the "_sequence" portion ns = list(ns) min_i = 0 rho = x**2 # c_Q is the leading term used to convert Qm to Qbfs c_Q = rho * (1 - rho) if ns[min_i] == 0: yield np.ones_like(x) * c_Q min_i += 1 if min_i == len(ns): return if ns[min_i] == 1: yield 1 / np.sqrt(19) * (13 - 16 * rho) * c_Q min_i += 1 if min_i == len(ns): return # c is the leading term of the recurrence relation for P c = 2 - 4 * rho # P0, P1 are the first two terms of the recurrence relation for auxiliary # polynomial P_n P0 = np.ones_like(x) * 2 P1 = 6 - 8 * rho Pnm2 = P0 Pnm1 = P1 # Q0, Q1 are the first two terms of the recurrence relation for Qbfs_n Q0 = np.ones_like(x) Q1 = 1 / np.sqrt(19) * (13 - 16 * rho) Qnm2 = Q0 Qnm1 = Q1 for nn in range(2, ns[-1] + 1): Pn = c * Pnm1 - Pnm2 Pnm2 = Pnm1 Pnm1 = Pn g = g_qbfs(nn - 1) h = h_qbfs(nn - 2) f = f_qbfs(nn) Qn = (Pn - g * Qnm1 - h * Qnm2) * ( 1 / f) # small optimization; mul by 1/f instead of div by f Qnm2 = Qnm1 Qnm1 = Qn if ns[min_i] == nn: yield Qn * c_Q min_i += 1 if min_i == len(ns): return
def jacobi_sequence(ns, alpha, beta, x): """Jacobi polynomials of orders ns with weight parameters alpha and beta. Parameters ---------- ns : iterable sorted polynomial orders to return, e.g. [1, 3, 5, 7, ...] alpha : float first weight parameter beta : float second weight parameter x : numpy.ndarray x coordinates to evaluate at Returns ------- generator equivalent to array of shape (len(ns), len(x)) """ # three key flavors: return list, return array, or return generator # return generator has most pleasant interface, benchmarked at 68 ns # per yield (315 clocks). With 32 clocks per element of x, 1% of the # time is spent on yield when x has 1000 elements, or 32x32 # => use generator # benchmarked at 4.6 ns/element (256x256), 4.6GHz CPU = 21 clocks # ~4x faster than previous impl (118 ms => 29.8) ns = list(ns) min_i = 0 Pn = np.ones_like(x) if ns[min_i] == 0: yield Pn min_i += 1 if min_i == len(ns): return Pn = alpha + 1 + (alpha + beta + 2) * ((x - 1) / 2) if ns[min_i] == 1: yield Pn min_i += 1 if min_i == len(ns): return Pnm1 = Pn A, B, C = recurrence_abc(1, alpha, beta) Pn = (A * x + B) * Pnm1 - C # no C * Pnm2 =because Pnm2 = 1 if ns[min_i] == 2: yield Pn min_i += 1 if min_i == len(ns): return max_n = ns[-1] for i in range(3, max_n + 1): Pnm2, Pnm1 = Pnm1, Pn A, B, C = recurrence_abc(i - 1, alpha, beta) Pn = (A * x + B) * Pnm1 - C * Pnm2 if ns[min_i] == i: yield Pn min_i += 1
def jacobi_der_sequence(ns, alpha, beta, x): """First partial derivative of Pn w.r.t. x for order ns, i.e. P_n'. Parameters ---------- ns : iterable sorted orders to return, e.g. [1, 2, 3, 10] returns P1', P2', P3', P10' alpha : float first weight parameter beta : float second weight parameter x : numpy.ndarray x coordinates to evaluate at Returns ------- generator equivalent to array of shape (len(ns), len(x)) """ # the body of this function is very similar to that of jacobi_sequence, # except note that der is related to jacobi n-1, # and the actual jacobi polynomial has a different alpha and beta # special note: P0 is invariant of alpha, beta # and within this function alphap1 and betap1 are "a+1" and "b+1" alphap1 = alpha + 1 betap1 = beta + 1 # except when it comes time to yield terms, we yield the modification # per A&S / the NIST link # and we modify the arguments to ns = list(ns) min_i = 0 if ns[min_i] == 0: # n=0 is piston, der==0 yield np.zeros_like(x) min_i += 1 if min_i == len(ns): return if ns[min_i] == 1: yield np.ones_like(x) * (0.5 * (1 + alpha + beta + 1)) min_i += 1 if min_i == len(ns): return # min_n is at least two, which means min n-1 is 1 # from here below, Pn is P of order i to keep the reader sane, but Pnm1 # is all that is needed; # therefor, Pn is computed only after testing if we are done and can return # to avoid a waste computation at the end of the loop # note that we can hardcode / unroll the loop up to n=3, one further than # in jacobi, because we use Pnm1 P1 = alphap1 + 1 + (alphap1 + betap1 + 2) * ((x - 1) / 2) if ns[min_i] == 2: yield P1 * (0.5 * (2 + alpha + beta + 1)) min_i += 1 if min_i == len(ns): return A, B, C = recurrence_abc(1, alphap1, betap1) P2 = (A * x + B) * P1 - C # no C * Pnm2 =because Pnm2 = 1 if ns[min_i] == 3: yield P2 * (0.5 * (3 + alpha + beta + 1)) min_i += 1 if min_i == len(ns): return # weird look just above P2, need to prepare for lower loop # by setting Pnm2 = P1, Pnm1 = P2 Pnm2 = P1 Pnm1 = P1 Pn = P2 # A, B, C = recurrence_abc(2, alpha, beta) # P3 = (A * x + B) * P2 - C * P1 # Pn = P3 max_n = ns[-1] for i in range(3, max_n + 1): Pnm2, Pnm1 = Pnm1, Pn if ns[min_i] == i: coef = 0.5 * (i + alpha + beta + 1) yield Pnm1 * coef min_i += 1 if min_i == len(ns): return A, B, C = recurrence_abc(i - 1, alphap1, betap1) Pn = (A * x + B) * Pnm1 - C * Pnm2