Skip to content

Commit 8b6fb19

Browse files
committed
add padua nodes
1 parent f27be87 commit 8b6fb19

3 files changed

Lines changed: 172 additions & 2 deletions

File tree

examples/plot-padua-nodes.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
from __future__ import annotations
2+
3+
import matplotlib.pyplot as plt
4+
import numpy as np
5+
6+
import modepy as mp
7+
8+
9+
def _first_padua_curve(n, t):
10+
return np.vstack([-np.cos((n + 1) * t), -np.cos(n * t)])
11+
12+
13+
def _second_padua_curve(n, t):
14+
return np.vstack([-np.cos(n * t), -np.cos((n + 1) * t)])
15+
16+
17+
def _third_padua_curve(n, t):
18+
return np.vstack([np.cos((n + 1) * t), np.cos(n * t)])
19+
20+
21+
def _fourth_padua_curve(n, t):
22+
return np.vstack([np.cos(n * t), np.cos((n + 1) * t)])
23+
24+
25+
def plot_padua_nodes(alpha, beta, order, family):
26+
if family == "first":
27+
curve_fn = _first_padua_curve
28+
elif family == "second":
29+
curve_fn = _second_padua_curve
30+
elif family == "third":
31+
curve_fn = _third_padua_curve
32+
elif family == "fourth":
33+
curve_fn = _fourth_padua_curve
34+
35+
t = np.linspace(0, np.pi, 512)
36+
curve = curve_fn(order, t)
37+
nodes = mp.padua_jacobi_nodes(alpha, beta, order, family)
38+
assert nodes.shape[1] == ((order + 1) * (order + 2) // 2)
39+
40+
fig = plt.figure()
41+
ax = fig.gca()
42+
ax.grid()
43+
44+
ax.plot(curve[0], curve[1])
45+
for i, xi in enumerate(nodes.T):
46+
ax.plot(nodes[0], nodes[1], "ko", markersize=8)
47+
ax.text(*xi, str(i), color="k", fontsize=24, fontweight="bold")
48+
49+
ax.set_xlim([-1, 1])
50+
ax.set_ylim([-1, 1])
51+
ax.set_aspect("equal")
52+
fig.savefig(f"padua_nodes_order_{order:02d}_family_{family}")
53+
plt.show()
54+
55+
56+
if __name__ == "__main__":
57+
import argparse
58+
59+
parser = argparse.ArgumentParser()
60+
parser.add_argument("--order", type=int, default=5)
61+
parser.add_argument("--family", default="first",
62+
choices=["first", "second", "third", "fourth"])
63+
parser.add_argument("--alpha", type=float, default=-0.5)
64+
parser.add_argument("--beta", type=float, default=-0.5)
65+
args = parser.parse_args()
66+
67+
plot_padua_nodes(args.alpha, args.beta, args.order, args.family)

modepy/__init__.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
THE SOFTWARE.
2525
"""
2626

27+
from pytools import MovedFunctionDeprecationWrapper
2728

2829
from modepy.matrices import (
2930
diff_matrices,
@@ -61,6 +62,8 @@
6162
legendre_gauss_lobatto_tensor_product_nodes,
6263
legendre_gauss_tensor_product_nodes,
6364
node_tuples_for_space,
65+
padua_jacobi_nodes,
66+
padua_nodes,
6467
random_nodes_for_shape,
6568
tensor_product_nodes,
6669
warp_and_blend_nodes,
@@ -167,6 +170,8 @@
167170
"nodal_quad_mass_matrix_for_face",
168171
"node_tuples_for_space",
169172
"orthonormal_basis_for_space",
173+
"padua_jacobi_nodes",
174+
"padua_nodes",
170175
"quadrature_for_space",
171176
"random_nodes_for_shape",
172177
"resampling_matrix",
@@ -181,7 +186,5 @@
181186
"warp_and_blend_nodes",
182187
]
183188

184-
from pytools import MovedFunctionDeprecationWrapper
185-
186189

187190
get_warp_and_blend_nodes = MovedFunctionDeprecationWrapper(warp_and_blend_nodes)

modepy/nodes.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@
3232
.. autofunction:: tensor_product_nodes
3333
.. autofunction:: legendre_gauss_tensor_product_nodes
3434
.. autofunction:: legendre_gauss_lobatto_tensor_product_nodes
35+
36+
.. autofunction:: padua_jacobi_nodes
37+
.. autofunction:: padua_nodes
3538
"""
3639

3740
from __future__ import annotations
@@ -432,6 +435,103 @@ def legendre_gauss_lobatto_tensor_product_nodes(dims: int, n: int) -> np.ndarray
432435
# }}}
433436

434437

438+
# {{{ Padua nodes
439+
440+
def _make_padua_grid_nodes(
441+
alpha: float, beta: float, order: int
442+
) -> tuple[np.ndarray, np.ndarray]:
443+
from modepy.quadrature.jacobi_gauss import jacobi_gauss_lobatto_nodes
444+
mu = jacobi_gauss_lobatto_nodes(alpha, beta, order)
445+
eta = jacobi_gauss_lobatto_nodes(alpha, beta, order + 1)
446+
447+
return mu, eta
448+
449+
450+
def _make_padua_jacobi_nodes(
451+
mu: np.ndarray, eta: np.ndarray, odd_or_even: int
452+
) -> np.ndarray:
453+
nodes = np.stack(np.meshgrid(mu, eta, indexing="ij"))
454+
indices = np.sum(
455+
np.meshgrid(np.arange(mu.size), np.arange(eta.size), indexing="ij"),
456+
axis=0)
457+
458+
return nodes[:, indices % 2 == odd_or_even].reshape(2, -1)
459+
460+
461+
def _first_padua_jacobi_nodes(alpha: float, beta: float, order: int) -> np.ndarray:
462+
mu, eta = _make_padua_grid_nodes(alpha, beta, order)
463+
return _make_padua_jacobi_nodes(mu, eta, 0)
464+
465+
466+
def _second_padua_jacobi_nodes(alpha: float, beta: float, order: int) -> np.ndarray:
467+
# NOTE: these are just "rotated" by pi/2 from the first family
468+
mu, eta = _make_padua_grid_nodes(alpha, beta, order)
469+
return _make_padua_jacobi_nodes(eta, mu, 0)
470+
471+
472+
def _third_padua_jacobi_nodes(alpha: float, beta: float, order: int) -> np.ndarray:
473+
# NOTE: these are just "rotated" by pi from the first family
474+
mu, eta = _make_padua_grid_nodes(alpha, beta, order)
475+
return _make_padua_jacobi_nodes(mu, eta, 1)
476+
477+
478+
def _fourth_padua_jacobi_nodes(alpha: float, beta: float, order: int) -> np.ndarray:
479+
# NOTE: these are just "rotated" by 2 pi/3 from the first family
480+
mu, eta = _make_padua_grid_nodes(alpha, beta, order)
481+
return _make_padua_jacobi_nodes(eta, mu, 1)
482+
483+
484+
def padua_jacobi_nodes(
485+
alpha: float, beta: float, order: int,
486+
family: str = "first") -> np.ndarray:
487+
r"""Generalized Padua-Jacobi nodes.
488+
489+
The Padua-Jacobi nodes are constructed from an interlaced grid of
490+
standard Jacobi-Gauss-Lobatto nodes, making use of
491+
:func:`~modepy.quadrature.jacobi_gauss.jacobi_gauss_lobatto_nodes`.
492+
This construction is detailed in
493+
494+
M. Briani, A. Sommariva, M. Vianello,
495+
*Computing Fekete and Lebesgue Points: Simplex, Square, Disk*,
496+
Journal of Computational and Applied Mathematics, Vol. 236,
497+
pp. 2477--2486, 2012, `DOI <http://dx.doi.org/10.1016/j.cam.2011.12.006>`_.
498+
499+
The values of the parameters :math:`(\alpha, \beta)` can have an effect
500+
on the Lebesgue constant of the resulting set, but all of them have
501+
optimal growth of :math:`\mathcal{O}(\log^2 n)`.
502+
503+
The Padua-Jacobi nodes are not rotationally symmetric.
504+
505+
:arg family: one of the four families of Padua-Jacobi nodes. The three
506+
additional families are :math:`90^\circ` rotations of the first one.
507+
"""
508+
509+
if family == "first":
510+
nodes = _first_padua_jacobi_nodes(alpha, beta, order)
511+
elif family == "second":
512+
nodes = _second_padua_jacobi_nodes(alpha, beta, order)
513+
elif family == "third":
514+
nodes = _third_padua_jacobi_nodes(alpha, beta, order)
515+
elif family == "fourth":
516+
nodes = _fourth_padua_jacobi_nodes(alpha, beta, order)
517+
else:
518+
raise ValueError(f"unknown Padua-Jacobi node family: '{family}'")
519+
520+
return nodes
521+
522+
523+
def padua_nodes(order: int, family: str = "first") -> np.ndarray:
524+
r"""Standard Padua nodes.
525+
526+
Padua nodes are Padua-Jacobi nodes with :math:`\alpha = \beta = -0.5`,
527+
i.e. they are constructed from the Chebyshev-Gauss-Lobatto nodes.
528+
"""
529+
530+
return padua_jacobi_nodes(-0.5, -0.5, order, family=family)
531+
532+
# }}}
533+
534+
435535
# {{{ space-based interface
436536

437537
@singledispatch

0 commit comments

Comments
 (0)