Skip to content
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
6be497c
First commit of search algorithm
jcs15c Sep 30, 2025
67fdfc6
Add test
jcs15c Oct 1, 2025
3f0f03c
Merge branch 'develop' into feature/spainhour/mfemless_gauss_legendre
jcs15c Oct 1, 2025
a0b488f
Add cache methods, rename to quadrature.hpp
jcs15c Oct 1, 2025
fe23580
Update comments, test
jcs15c Oct 2, 2025
651fe3f
Update integral methods to use MFEM if available
jcs15c Oct 3, 2025
d90bab0
Improve comments
jcs15c Oct 3, 2025
b5b199f
Re-add missing headers from CMakeLists.txt
jcs15c Oct 3, 2025
f0df642
Merge branch 'develop' into feature/spainhour/mfemless_gauss_legendre
jcs15c Oct 3, 2025
ac385b3
Temporarily revert test
jcs15c Oct 3, 2025
0d23e1f
Merge branch 'develop' into feature/spainhour/mfemless_gauss_legendre
jcs15c Oct 3, 2025
12ee22b
Merge branch 'feature/spainhour/mfemless_gauss_legendre' of https://g…
jcs15c Oct 3, 2025
e90dc6c
Move test to other suite
jcs15c Oct 3, 2025
68002f3
More testing
jcs15c Oct 3, 2025
cc857c6
Another test check
jcs15c Oct 3, 2025
e300d22
Try to restore the test with necessary includes
jcs15c Oct 3, 2025
be3c09a
Fix some includes
jcs15c Oct 3, 2025
49677e9
Remove the rest of MFEM from evaluate_integral
jcs15c Oct 6, 2025
6c54654
Swap to axom::FlatMap
jcs15c Oct 6, 2025
03b9ebe
Use new method in LinearizeCurves
jcs15c Oct 7, 2025
6474280
Remove unnecessary member data
jcs15c Oct 7, 2025
85f995e
Fix comments, asserts, numeric limits
jcs15c Oct 7, 2025
540471f
Merge branch 'develop' into feature/spainhour/mfemless_gauss_legendre
jcs15c Oct 7, 2025
55ad9f4
Make an ArrayView version
jcs15c Oct 7, 2025
095f658
Remove unnecessary mfem include
jcs15c Oct 7, 2025
5396af2
Finish removing mfem from LinearizeCurves.cpp
jcs15c Oct 8, 2025
cf06670
Update to use views and allocator IDs
jcs15c Oct 10, 2025
1caf098
fix test
jcs15c Oct 10, 2025
aa242b1
Correct prints, add decorators
jcs15c Oct 10, 2025
10eac22
Push debugging change
jcs15c Oct 10, 2025
cdda869
Fix a missing allocatorID pass
jcs15c Oct 10, 2025
95dab9d
Remove debugging statement
jcs15c Oct 10, 2025
c03dab0
Merge branch 'develop' into feature/spainhour/mfemless_gauss_legendre
jcs15c Oct 21, 2025
69620c6
Improve comment quality
jcs15c Oct 23, 2025
b474900
Merge branch 'develop' into feature/spainhour/mfemless_gauss_legendre
jcs15c Oct 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions RELEASE-NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ The Axom project release numbers follow [Semantic Versioning](http://semver.org/
- Adds `quest::MFEMReader` for reading 1D MFEM contours in 2D space.
- Adds an option to `quest::SamplingShaper` to allow in/out tests based on winding numbers for MFEM contours.
- The `shaping_driver` example program can select `--sampling inout` to do the default In/Out sampling and `--sampling windingnumber` to select winding number in/out tests for MFEM data.
- Adds Axom-native Gauss-Legendre quadrature rules that can be used without an MFEM dependency

### Changed
- Updates blt submodule to [BLT version 0.7.1][https://github.com/LLNL/blt/releases/tag/v0.7.1]
Expand Down
2 changes: 2 additions & 0 deletions src/axom/core/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ set(core_headers
numerics/eigen_solve.hpp
numerics/eigen_sort.hpp
numerics/floating_point_limits.hpp
numerics/quadrature.hpp
numerics/jacobi_eigensolve.hpp
numerics/linear_solve.hpp
numerics/matvecops.hpp
Expand Down Expand Up @@ -106,6 +107,7 @@ set(core_sources
${PROJECT_BINARY_DIR}/axom/core/utilities/About.cpp

numerics/polynomial_solvers.cpp
numerics/quadrature.cpp

Path.cpp
Types.cpp
Expand Down
150 changes: 150 additions & 0 deletions src/axom/core/numerics/quadrature.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
// Copyright (c) 2017-2025, Lawrence Livermore National Security, LLC and
// other Axom Project Developers. See the top-level LICENSE file for details.
//
// SPDX-License-Identifier: (BSD-3-Clause)

#include "axom/core/Array.hpp"
#include "axom/core/FlatMap.hpp"
#include "axom/core/numerics/quadrature.hpp"

// For math constants and includes
#include "axom/config.hpp"
#include <cmath>

namespace axom
{
namespace numerics
{

/*!
* \brief Computes a 1D quadrature rule of Gauss-Legendre points
*
* \param [in] npts The number of points in the rule
*
* A Gauss-Legendre rule with \a npts points can exactly integrate
* polynomials of order 2 * npts - 1
*
* Algorithm adapted from the MFEM implementation in `mfem/fem/intrules.cpp`
*
* \note This method constructs the points by scratch each time, without caching
* \sa get_gauss_legendre(int)
*
* \return The `QuadratureRule` object which contains axom::Array<double>'s of nodes and weights
*/
QuadratureRule compute_gauss_legendre(int npts)
{
QuadratureRule rule(npts);

if(npts == 1)
{
rule.m_nodes[0] = 0.5;
rule.m_weights[0] = 1.0;
return rule;
}
if(npts == 2)
{
rule.m_nodes[0] = 0.21132486540518711775;
rule.m_nodes[1] = 0.78867513459481288225;

rule.m_weights[0] = rule.m_weights[1] = 0.5;
return rule;
}
if(npts == 3)
{
rule.m_nodes[0] = 0.11270166537925831148207345;
rule.m_nodes[1] = 0.5;
rule.m_nodes[2] = 0.88729833462074168851792655;

rule.m_weights[0] = 0.2777777777777777777777778;
rule.m_weights[1] = 0.4444444444444444444444444;
rule.m_weights[2] = 0.2777777777777777777777778;
return rule;
}

const int n = npts;
const int m = (npts + 1) / 2;

// Nodes are mirrored across x = 0.5, so only need to evaluate half
for(int i = 1; i <= m; ++i)
{
// Each node is the root of a Legendre polynomial,
// which are approximately uniformly distirbuted in arccos(xi).
// This makes cos a good initial guess for subsequent Newton iterations
double z = std::cos(M_PI * (i - 0.25) / (n + 0.5));
double Pp_n, P_n, dz, xi = 0.0;

bool done = false;
while(true)
{
// Recursively evaluate P_n(z) through the recurrence relation
// n * P_n(z) = (2n - 1) * P_{n-1}(z) - (n - 1) * P_{n - 2}(z)
double P_nm1 = 1.0; // P_0(z) = 1
P_n = z; // P_1(z) = z
for(int j = 2; j <= n; ++j)
{
double P_nm2 = P_nm1;
P_nm1 = P_n;
P_n = ((2 * j - 1) * z * P_nm1 - (j - 1) * P_nm2) / j;
}

// Evaluate P'_n(z) using another recurrence relation
// (z^2 - 1) * P'_n(z) = n * z * P_n(z) - n * P_{n-1}(z)
Pp_n = n * (z * P_n - P_nm1) / (z * z - 1);

if(done)
{
break;
}

// Compute the Newton method step size
dz = P_n / Pp_n;

if(std::fabs(dz) < 1e-16)
Copy link
Contributor

Choose a reason for hiding this comment

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

This is the termination condition. I feel hesitant to hard-wire the error tolerance to 1e-16. Thoughts?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Do you think replacing it with a axom::numerics::floating_point_limits<double>::epsilon() is better? I personally don't think it needs to be/should be an exposed tolerance, since its ambiguous (to me at least) how using approximate quadrature nodes impacts the accuracy of the downstream numerical integration.

As it is, the algorithm already converges very quickly, and is very insensitive to the numerical stopping criteria. If you replace it with 10^-10, you get essentially the same number of iterations, since Newton's method converges quadratically.

{
done = true;

// Scale back to [0, 1]
xi = ((1 - z) + dz) / 2;
}

// Take the Newton step, repeat the process
z -= dz;
}

rule.m_nodes[i - 1] = xi;
rule.m_nodes[n - i] = 1.0 - xi;

// Evaluate the weight as w_i = 2 / (1 - z^2) / P'_n(z)^2, with z \in [-1, 1]
rule.m_weights[i - 1] = rule.m_weights[n - i] = 1.0 / (4.0 * xi * (1.0 - xi) * Pp_n * Pp_n);
}

return rule;
}

/*!
* \brief Computes or accesses a precomputed 1D quadrature rule of Gauss-Legendre points
*
* \param [in] npts The number of points in the rule
*
* A Gauss-Legendre rule with \a npts points can exactly integrate
* polynomials of order 2 * npts - 1
*
* \note If this method has already been called for a given order, it will reuse the same quadrature points
* without needing to recompute them
*
* \return The `QuadratureRule` object which contains axom::Array<double>'s of nodes and weights
*/
const QuadratureRule& get_gauss_legendre(int npts)
{
// Define a static map that stores the GL quadrature rule for a given order
static axom::FlatMap<int, QuadratureRule> rule_library;
if(rule_library.find(npts) == rule_library.end())
{
rule_library[npts] = compute_gauss_legendre(npts);
}

return rule_library[npts];
}

} /* end namespace numerics */
} /* end namespace axom */
88 changes: 88 additions & 0 deletions src/axom/core/numerics/quadrature.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// Copyright (c) 2017-2025, Lawrence Livermore National Security, LLC and
// other Axom Project Developers. See the top-level LICENSE file for details.
//
// SPDX-License-Identifier: (BSD-3-Clause)

#ifndef AXOM_NUMERICS_QUADRATURE_HPP_
#define AXOM_NUMERICS_QUADRATURE_HPP_

#include "axom/core/Array.hpp"

/*!
* \file quadrature.hpp
* The functions declared in this header file find the nodes and weights of
* arbitrary order quadrature rules
*/

namespace axom
{
namespace numerics
{

/*!
* \class QuadratureRule
*
* \brief Stores a fixed array of 1D quadrature points and weights
*/
class QuadratureRule
{
// Define friend functions so rules can only be created via compute_rule() methods
friend QuadratureRule compute_gauss_legendre(int npts);

public:
QuadratureRule() = default;

//! \brief Accessor for quadrature nodes
double node(size_t idx) const { return m_nodes[idx]; };

//! \brief Accessor for quadrature weights
double weight(size_t idx) const { return m_weights[idx]; };

//! \brief Accessofr for the size of the quadrature rule
int getNumPoints() const { return m_npts; }

private:
//! \brief Private constructor for use in compute_<rule>() methods. Only allocates memory
QuadratureRule(int npts) : m_nodes(npts), m_weights(npts), m_npts(npts) { };

axom::Array<double> m_nodes;
Copy link
Member

@BradWhitlock BradWhitlock Oct 6, 2025

Choose a reason for hiding this comment

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

I would prefer to see QuadratureRule use axom::ArrayView<double> instead. The array ownership could be still done in your friend function. That function should probably also take an optional allocatorID. Then I'd like to see QuadratureRule decorated with AXOM_HOST_DEVICE and tested in some GPU kernels.

Or maybe instead of doing that, provide a method for returning a view of the QuadratureRule.

struct QuadratureRule
{
  struct View
  { AXOM_HOST_DEVICE double node(axom::IndexType index) const { return m_nodes[index]; }
    AXOM_HOST_DEVICE double weight(axom::IndexType index) const { return m_weights[index]; }
    AXOM_HOST_DEVICE axom::IndexType size() const { return m_nodes.size(); }
    axom::ArrayView<double> m_nodes, m_weights;
  };

  View view() const
  {
    return View{m_nodes.view(), m_weights.view()};
  }
};

Copy link
Member

Choose a reason for hiding this comment

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

Another thing - since the axom::Array contains a size, why also have a member to keep track of the number of points?

Copy link
Member

Choose a reason for hiding this comment

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

I think you answered my npts question above. n/m

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No, you're totally right, QuadratureRule::size() (or .getNumPoints()) should just call m_nodes.size() (which is always equal to m_weigths.size(), regardless of the method) instead of storing the extra integer. If it was an actual .getOrder() method, then it would have to be separate like this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@BradWhitlock To your earlier point about views, just to make sure I understand correctly, that would that require that the static unordered_map in get_gauss_legendre instead have values of a pair of axom::Arrays (or two maps of one array each), so that the QuadratureRule object can contain only views of those arrays, right? That would work, but I think I'd have to make calculate_gauss_legendre return the arrays themselves instead of a QuadratureRule object, or populate a const reference to the map, or just move its entire functionality into get_gauss_legendre.

My original idea was to let calculate_gauss_legendre bypass the caching altogether and just compute the nodes on the fly, but I'm not sure what the utility of that would really be. Let me implement this view version and see if it's closer to what you have in mind.

Copy link
Member

@BradWhitlock BradWhitlock Oct 7, 2025

Choose a reason for hiding this comment

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

Hi @jcs15c , I think you get what I mean but in case this helps clear it up...

// template it in case we wanted to call from a kernel and pass axom::StackArray or axom::StaticArray.
template <typename Container>
AXOM_HOST_DEVICE
void calculate_gauss_legendre(int npts, Container &nodes, Container &weights)
{
   // existing logic using these output arrays.
}

QuadratureRule get_gauss_legendre(int npts, int allocatorID = <some default value>)
{
   // Assumes std::map<std::pair<int,int>, std::pair<axom::Array<double>,axom::Array<double>>>;
   const auto key = std::make_pair<int,int>(npts, allocatorID);
   auto it = cache.find(key);
   if(it == cache.end())
   {
     it = cache[key];

     auto nodes = axom::Array<double>(npts);
     auto weights = axom::Array<double>(npts);
     calculate_gauss_legendre(npts, nodes, weights);

     // copy nodes, weights to arrays in target memory space
     it->second.first = axom::Array<double>(npts, npts, allocatorID);
     it->second.second = axom::Array<double>(npts, npts, allocatorID);
     axom::copy(it->second.first.data(), nodes.data(), npts * sizeof(double));
     axom::copy(it->second.second.data(), weights.data(), npts * sizeof(double));
   }
   return QuadratureRule{it->second.first.view(), it->second.second.view()};
}

Then later in other code, we could do
template <typename ExecSpace>
void func()
{
   // Get the rule for the right memory space, creating it if needed.
   // I was assuming that the function would be called prior to the axom::for_all. If we need to
   // call it for different numbers of points from inside the lambda then that is a problem with
   // what I proposed.
   const auto rule = get_gauss_legendre(5, axom::execution_space<ExecSpace>::allocatorID());

   // Use the rule in a lambda
   axom::for_all<ExecSpace>(n, AXOM_LAMBDA(axom::IndexType i)
   {
     double sum = 0.;
     for(int j = 0; j < rule.numPoints(); j++)
     {
        // These QuadratureRule methods would be AXOM_HOST_DEVICE to use them here
        sum += rule.weight(j) * f(rule.node(j));

        // Maybe we can do this now too...
        axom::StackArray<double, MAXNODES> nodes, weights;
        int nnodes = // something dynamic
        calculate_gauss_legendre(nnodes, nodes, weights);
        QuadratureRule newQR{axom::ArrayView<double>(nodes.data(), nnodes),
                                         axom::ArrayView<double>(weights.data(), nnodes)};
        // use newQR
     }
     someArray[i] = sum;
   });
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Okay great, I think I got it pretty close then, aside from a handful of redundant cache lookups. Thank you!!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@BradWhitlock Would you mind taking another look at my implementation of the ArrayView version, and the test I'm using in numerics_quadrature.hpp (reproduced below) just to make sure this looks practical for future applications? In particular, I don't think it's necessary to be able generate rules from within the lambda, since the generation procedure isn't especially expensive, and by definition only needs to be done once for a given rule.

On the other hand, the node computation procedure within a certain rule is pretty well parallelizeable, and could hypothetically benefit from being ported to the GPU, but I don't think there's many contexts in which anyone could use a rule with an order high enough to make it worth it. Even generating ~100,000 points only takes about half a minute, and that was on a debug build.

template <typename ExecSpace>
struct test_device_quadrature
{
  static void test()
  {
    const int npts = 15;

    // Create the rule with the proper allocator
    const auto rule =
      axom::numerics::get_gauss_legendre(npts, axom::execution_space<ExecSpace>::allocatorID());

    // Use the rule in a lambda to integrate the volume under std::sin(pi * x) on [0, 1]
    axom::ReduceSum<ExecSpace, double> quadrature_sum(0.0);
    axom::for_all<ExecSpace>(
      npts,
      AXOM_LAMBDA(axom::IndexType i) { quadrature_sum += rule.weight(i) * sin(rule.node(i)); });

    EXPECT_NEAR(quadrature_sum.get(), 0.459697694132, 1e-6);
  }
};

TEST(numerics_quadrature, get_nodes_seq) { test_device_quadrature<axom::SEQ_EXEC>::test(); }

#if defined(AXOM_RUNTIME_POLICY_USE_OPENMP)
TEST(numerics_quadrature, get_nodes_omp) { test_device_quadrature<axom::OMP_EXEC>::test(); }
#endif

#if defined(AXOM_RUNTIME_POLICY_USE_CUDA)
TEST(numerics_quadrature, get_nodes_cuda) { test_device_quadrature<axom::CUDA_EXEC<256>>::test(); }
#endif

#if defined(AXOM_RUNTIME_POLICY_USE_HIP)
TEST(numerics_quadrature, get_nodes_hip) { test_device_quadrature<axom::HIP_EXEC<256>>::test(); }
#endif

axom::Array<double> m_weights;
int m_npts;
};

/*!
* \brief Computes a 1D quadrature rule of Gauss-Legendre points
*
* \param [in] npts The number of points in the rule
*
* A Gauss-Legendre rule with \a npts points can exactly integrate
* polynomials of order 2 * npts - 1
*
* Algorithm adapted from the MFEM implementation in `mfem/fem/intrules.cpp`
*
* \note This method constructs the points by scratch each time, without caching
* \sa get_gauss_legendre(int)
*
* \return The `QuadratureRule` object which contains axom::Array<double>'s of nodes and weights
*/
QuadratureRule compute_gauss_legendre(int npts);

/*!
* \brief Computes or accesses a precomputed 1D quadrature rule of Gauss-Legendre points
*
* \param [in] npts The number of points in the rule
*
* A Gauss-Legendre rule with \a npts points can exactly integrate
* polynomials of order 2 * npts - 1
*
* \note If this method has already been called for a given order, it will reuse the same quadrature points
* without needing to recompute them
*
* \return The `QuadratureRule` object which contains axom::Array<double>'s of nodes and weights
*/
const QuadratureRule& get_gauss_legendre(int npts);

} /* end namespace numerics */
} /* end namespace axom */

#endif // AXOM_NUMERICS_QUADRATURE_HPP_
1 change: 1 addition & 0 deletions src/axom/core/tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ set(core_serial_tests
numerics_eigen_sort.hpp
numerics_floating_point_limits.hpp
numerics_jacobi_eigensolve.hpp
numerics_quadrature.hpp
numerics_linear_solve.hpp
numerics_lu.hpp
numerics_matrix.hpp
Expand Down
1 change: 1 addition & 0 deletions src/axom/core/tests/core_serial_main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
#include "numerics_matrix.hpp"
#include "numerics_matvecops.hpp"
#include "numerics_polynomial_solvers.hpp"
#include "numerics_quadrature.hpp"

#include "utils_endianness.hpp"
#include "utils_fileUtilities.hpp"
Expand Down
104 changes: 104 additions & 0 deletions src/axom/core/tests/numerics_quadrature.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// Copyright (c) 2017-2025, Lawrence Livermore National Security, LLC and
// other Axom Project Developers. See the top-level LICENSE file for details.
//
// SPDX-License-Identifier: (BSD-3-Clause)

#include "gtest/gtest.h"

#include "axom/core/numerics/quadrature.hpp"
#include "axom/core/utilities/Utilities.hpp"

TEST(numerics_quadrature, gauss_legendre_math_check)
{
const int N = 200;

double coeffs[2 * N - 1];

// Test that the rules provide exact integration for polynomials of degree 2n - 1
for(int npts = 1; npts <= N; ++npts)
{
// Evaluate using the quadrature rule
auto rule = axom::numerics::get_gauss_legendre(npts);
int degree = 2 * npts - 1;

// Define a collection of random coefficients for a polynomial
for(int j = 0; j < degree; ++j)
{
// Seed the random coefficients for consistency in the test
coeffs[j] = axom::utilities::random_real(-1.0, 1.0, npts + j);
}

// Evaluate the area under the curve from 0 to 1 analytically
double analytic_result = 0.0;
for(int j = 0; j < degree; ++j)
{
analytic_result += coeffs[j] / (j + 1);
}

// Evaluate the polynomial using Horner's rule
auto eval_polynomial = [&degree, &coeffs](double x) {
double result = coeffs[degree - 1];
for(int i = degree - 2; i >= 0; --i)
{
result = result * x + coeffs[i];
}
return result;
};

double quadrature_result = 0.0;
for(int j = 0; j < npts; ++j)
{
quadrature_result += rule.weight(j) * eval_polynomial(rule.node(j));
}

EXPECT_NEAR(quadrature_result, analytic_result, 1e-10);

// Check that nodes aren't duplicated
for(int j = 1; j < npts; ++j)
{
EXPECT_GT(rule.node(j), rule.node(j - 1));
}

// Check that the sum of the weights is 1, and that all are positive
double weight_sum = 0.0;
for(int j = 0; j < npts; ++j)
{
EXPECT_GT(rule.weight(j), 0.0);
weight_sum += rule.weight(j);
}

EXPECT_NEAR(weight_sum, 1.0, 1e-10);

// Check that each node is the root of the next Legendre polynomial
for(int j = 0; j < npts; ++j)
{
// Rescale the node to [-1, 1]
double z = 2 * rule.node(j) - 1;

double P_n = z, P_nm1 = 1.0;
for(int k = 2; k <= npts; ++k)
{
double P_nm2 = P_nm1;
P_nm1 = P_n;
P_n = ((2 * k - 1) * z * P_nm1 - (k - 1) * P_nm2) / k;
}

// Note that this metric does not directly imply that |z - z*| < tol
EXPECT_NEAR(P_n, 0.0, 1e-10);
}
}
}

TEST(numerics_quadrature, gauss_legendre_cache_check)
{
// The first two rules are put in the cache
const axom::numerics::QuadratureRule& first_rule = axom::numerics::get_gauss_legendre(20);
const axom::numerics::QuadratureRule& second_rule = axom::numerics::get_gauss_legendre(20);

// The third is not
const axom::numerics::QuadratureRule& third_rule = axom::numerics::compute_gauss_legendre(20);

// Check that the two rules are located in the same place in memory, and the third isn't
EXPECT_EQ(&first_rule, &second_rule);
EXPECT_NE(&first_rule, &third_rule);
}
Loading