Skip to content

Revise and document the one-dimensional quadrature code - #1095

Open
garth-wells wants to merge 2 commits into
mainfrom
revise-quadrature-code
Open

garth-wells wants to merge 2 commits into
mainfrom
revise-quadrature-code

Conversation

@garth-wells

Copy link
Copy Markdown
Member

Closes #570.

The helpers that build the one-dimensional rules are documented, and the functions that duplicated each other are removed. The points and weights are unchanged.

Duplication removed

  • compute_jacobi_deriv evaluated (P_n^{a,0}) and its derivatives at an array of points, allocating (nderiv + 1, n + 1, npoints) and (n + 1, npoints) arrays. Its only caller asked for one derivative and read a single row of the result. jacobi_value_deriv already computes exactly that from the same recurrence, keeping only the last two values of each sequence, so compute_jacobi_deriv is deleted.
  • The map from [-1, 1] to [0, 1] existed in three places: make_quadrature_line, quadrature::gauss_jacobi_rule and quadrature::get_gl_points, each written slightly differently. They now share one helper, gauss_jacobi_rule_01.
  • compute_gll_rule is folded into make_gll_line, its only caller.
  • The tensor-product loops for quadrilaterals and hexahedra were four copies of the same nested loops, in the Gauss-Jacobi and GLL paths. They now call a single tensor_product helper.

Also dropped: the now-unused mdarray_t alias, an unused local in compute_jacobi_deriv, a commented-out line in compute_gll_rule, and two returns by copy that should have been moves. M_PI, which is POSIX rather than standard C++, is replaced by std::numbers::pi.

Documentation

Every helper now states the domain it works on, the weight function, how many points it uses and the degree it is exact to, and cites the algorithm it implements. The collapsed triangle and tetrahedron rules get the Duffy map written out, along with its Jacobian determinant and the reason the Gauss-Jacobi weight exponents are 1 and 2. The @todo on gauss is resolved. On the public side, get_gll_points now documents the ordering of the points it returns, which was not recorded anywhere: the two endpoints come first, in the order 0, 1, followed by the interior points in increasing order.

The Newton iteration in compute_gauss_jacobi_points keeps its tolerance of 1e-8, now with a comment explaining it: quadratic convergence means an update that small leaves a root accurate to roughly 1e-16. The comment also records that in single precision the update can stagnate at the level of roundoff without reaching the tolerance, so the iteration runs to max_iter with the root already accurate to single precision.

The numbers are unchanged

Checked by dumping every rule from the library built before and after, as hex float:

  • all seven cell types × all seven quadrature types × both polyset types × degrees 0 to 24,
  • get_gl_points, get_gll_points and gauss_jacobi_rule with a from 0 to 3,
  • in both float and double.

That is 3,142,000 double values and 3,133,542 float values, every one of them bit-identical.

Several changes had to be written carefully to keep that true, because the literals in an expression decide whether it is evaluated in double: the original code mixes 1.0 and 0.5 literals into float expressions, so the intermediate arithmetic is done in double and only the result is narrowed. The literals are therefore left as they were.

Tests

Two cases added to cpp/test/quadrature.cpp, covering the two paths that changed shape rather than just gaining comments:

  • the quadrilateral and hexahedron rules are the tensor product of the interval rule, with the multi-index ordering the new helper promises;
  • get_gl_points(m) agrees with the interval rule that uses m points.

🤖 Generated with Claude Code

Document each of the helpers that build the one-dimensional rules,
stating the domain, the weight function, the degree of exactness and
the source of the algorithm, and remove several functions that
duplicate each other.

- Remove compute_jacobi_deriv. Its only caller asked for one
  derivative and read a single row of the result, which
  jacobi_value_deriv already computes without allocating
  (nderiv + 1, n + 1, npoints) arrays.
- Add gauss_jacobi_rule_01, the rule on [0, 1], so that
  make_quadrature_line and quadrature::gauss_jacobi_rule share one
  implementation of the map from [-1, 1] rather than carrying two
  slightly different copies of it. get_gl_points reuses it too,
  instead of repeating the map a third time.
- Fold compute_gll_rule into make_gll_line, its only caller.
- Add tensor_product and use it for the quadrilateral and hexahedron
  cases of the Gauss-Jacobi and GLL rules, which were four copies of
  the same nested loops.
- Drop the unused mdarray_t alias, an unused local and a commented-out
  line, return by move rather than by copy from the GLL rule, and
  replace the POSIX M_PI with std::numbers::pi.

Also document the ordering of the points returned by get_gll_points,
and add tests that the quadrilateral and hexahedron rules are the
tensor product of the interval rule and that get_gl_points agrees with
the interval rule.

The points and weights are unchanged. Every rule, for all cell types,
quadrature types, polyset types and degrees up to 24, along with
get_gl_points, get_gll_points and gauss_jacobi_rule, is bit-identical
to before in both single and double precision.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@garth-wells
garth-wells marked this pull request as ready for review October 17, 2026 18:22
State the formulas the one-dimensional routines implement, rather than
only naming the algorithms, so that the code can be checked against the
documentation line by line.

- rec_jacobi: give the closed forms of alpha_k and beta_k, and the
  zeroth moment of the weight that fixes beta_0. Note that these are
  the monic polynomials, scaled differently from the P_k^(a,b) that
  jacobi_value_deriv evaluates.
- gauss: write out the Jacobi matrix and the weight formula, and say
  that beta[0] does not appear in the matrix.
- lobatto: give the continued fraction g(y) = -pi_{n-2}(y)/pi_{n-1}(y)
  and the replacements for the last pair of coefficients.
- jacobi_value_deriv: give the three-term recurrence with its
  coefficients and the differentiated recurrence, and rename the local
  variables to match.
- compute_gauss_jacobi_points: derive the deflated Newton update and
  say why it cannot return to a root already found.
- compute_gauss_jacobi_rule, gauss_jacobi_rule_01, make_quadrature_line,
  make_gll_line: state the identity each rule satisfies and the degree
  it holds to, and derive the scaling of the weights under the map to
  [0, 1].
- The collapsed triangle and tetrahedron rules: write out the change of
  variables and count the degree of exactness, which is 2m - 1 and no
  higher.
- The public interval functions in quadrature.h likewise.

The claims were checked against the code in 50-digit arithmetic: the
recurrence against mpmath's Jacobi polynomials, the quadrature weight
formula for integer and non-integer exponents, and each stated degree
of exactness, confirming it is attained and not exceeded.

That check found an off-by-one in lobatto, which used alpha[i] where
the recurrence for the ratio of consecutive polynomials needs
alpha[i - 1]. The prescribed points were then not reproduced unless
alpha is identically zero. Fixed here. The only caller passes the
Legendre coefficients, for which every alpha is zero, so no rule
changes: the points and weights remain bit-identical for all cells,
rules and degrees, in single and double precision.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@garth-wells

Copy link
Copy Markdown
Member Author

Pushed a second commit expanding the mathematics in the documentation, and checking it.

Each routine now states the formula it implements rather than only naming the algorithm, so the code can be read against the documentation line by line: the closed forms of the recurrence coefficients and the zeroth moment that fixes beta_0; the Jacobi matrix and weight formula of the Golub-Welsch step; the continued fraction and coefficient replacement in the Lobatto step; the three-term recurrence and its differentiated form, with the local variables renamed to match the symbols; the derivation of the deflated Newton update; the identity and degree of exactness of each rule; the change of variables and degree count for the collapsed triangle and tetrahedron rules.

The statements were checked, not just written. In 50-digit arithmetic: the recurrence against mpmath's Jacobi polynomials; the quadrature weight formula w_i = 2^(a+1) / ((1 - x_i^2) [P_m^(a,0)'(x_i)]^2) for a = 0, 1, 2, 3, 0.5 and 2.75 against the exact integrals; and every stated degree of exactness, confirming in each case that it is attained and that the next degree is not.

That check turned up a real off-by-one in lobatto. The continued fraction for the ratio of consecutive orthogonal polynomials needs alpha[i - 1], but the code used alpha[i] (and alpha[n - 1] instead of alpha[n - 2] in the final step). Against Gautschi's reference implementation:

weight n as implemented fixed reference
(1-x)^0 (1+x)^0 5 [-1, 1] [-1, 1] [-1, 1]
(1-x)^1 (1+x)^0 5 [-1.020924, 0.98995258] [-1, 1] [-1, 1]
(1-x)^2 (1+x)^0 5 [-1.0346843, 0.98487798] [-1, 1] [-1, 1]
(1-x)^0.5 (1+x)^1.5 5 [-0.99043547, 1.0152268] [-1, 1] [-1, 1]

So the prescribed points were not reproduced for any weight that is not symmetric about the origin. The only caller passes the Legendre coefficients, where every alpha is zero and the index therefore does not matter, so this was latent rather than live, and the fix changes no output: the points and weights are still bit-identical to main across the full sweep described above, in both precisions.

With the fix the continued fraction equals -pi_{n-2}(y) / pi_{n-1}(y) for every weight tested, which is what the documentation now claims.

Doxygen builds clean with WARN_AS_ERROR = YES.

Comment thread cpp/basix/quadrature.cpp
/// for every polynomial \f$f\f$ of degree at most \f$2 m - 3\f$.
/// Prescribing the two endpoints costs two degrees relative to the
/// Gauss-Legendre rule of ::make_quadrature_line on the same number of
/// points. It is what makes the rule useful for spectral element

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes the rule useful...

Comment thread cpp/basix/quadrature.cpp
/// d\f$, which is of degree at most \f$d\f$ in \f$u\f$ and at most
/// \f$d\f$ in \f$v\f$. Both one-dimensional rules are exact to
/// degree \f$2 m - 1\f$, so the \f$m^2\f$-point rule is exact for
/// polynomials of total degree \f$2 m - 1\f$, and no higher. It

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It - Duffy transformed rules therefore require more points than a rule designed specifically for the triangle.

Comment thread cpp/basix/quadrature.cpp
/// alongside a Gauss-Legendre rule in \f$u\f$. By the same counting
/// as for the triangle, the \f$m^3\f$-point rule is exact for
/// polynomials of total degree \f$2 m - 1\f$, and no higher.
///

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add: Collapsed rules therefore require more points than a rule designed specifically for the tetrahedron.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Revise quadrature code

2 participants