SpECTRE Documentation Coverage Report
Current view: top level - NumericalAlgorithms/LinearOperators - PowerMonitors.hpp Hit Total Coverage
Commit: 107e15b340886ae54549b1baa4bfc92e676f667e Lines: 19 22 86.4 %
Date: 2026-09-17 16:38:56
Legend: Lines: hit not hit

          Line data    Source code
       1           0 : // Distributed under the MIT License.
       2             : // See LICENSE.txt for details.
       3             : 
       4             : #pragma once
       5             : 
       6             : #include <array>
       7             : #include <cstddef>
       8             : #include <limits>
       9             : #include <vector>
      10             : 
      11             : #include "DataStructures/DataVector.hpp"
      12             : #include "NumericalAlgorithms/Spectral/Mesh.hpp"
      13             : #include "NumericalAlgorithms/TensorYlm/Helpers.hpp"
      14             : #include "Utilities/ConstantExpressions.hpp"
      15             : #include "Utilities/Gsl.hpp"
      16             : #include "Utilities/TMPL.hpp"
      17             : 
      18             : /*!
      19             :  * \brief Items for assessing truncation error in spectral methods.
      20             :  */
      21           1 : namespace PowerMonitors {
      22             : 
      23             : /// @{
      24             : /*!
      25             :  * \ingroup SpectralGroup
      26             :  * \brief Returns array of power monitors in each spatial dimension.
      27             :  *
      28             :  * Computed following Sec. 5.1 of Ref. \cite Szilagyi2014fna.
      29             :  * For example, in the x dimension (indexed by \f$ k_0 \f$), we compute
      30             :  *
      31             :  * \f{align*}{
      32             :  *  P_{k_0}[\psi] = \sqrt{ \frac{1}{N_1 N_2}
      33             :  *   \sum_{k_1,k_2} \left| C_{k_0,k_1,k_2} \right|^2} ,
      34             :  * \f}
      35             :  *
      36             :  * where \f$ C_{k_0,k_1,k_2}\f$ are the modal coefficients
      37             :  * of variable \f$ \psi \f$.
      38             :  *
      39             :  * For dimensions using a Fourier basis, the cosine and sine power for each
      40             :  * wavenumber \f$ k \f$ are combined via \f$ P_k = \sqrt{P_{\cos,k}^2 +
      41             :  * P_{\sin,k}^2} \f$, so the output size for a Fourier dimension of \f$ N \f$
      42             :  * points is \f$ N/2 + 1 \f$ (integer division) rather than \f$ N \f$.
      43             :  *
      44             :  */
      45             : template <typename VectorType, size_t Dim>
      46           1 : void power_monitors(gsl::not_null<std::array<DataVector, Dim>*> result,
      47             :                     const VectorType& u, const Mesh<Dim>& mesh);
      48             : 
      49             : template <typename VectorType, size_t Dim>
      50           1 : std::array<DataVector, Dim> power_monitors(const VectorType& u,
      51             :                                            const Mesh<Dim>& mesh);
      52             : /// @}
      53             : 
      54             : /// @{
      55             : /*!
      56             :  * \ingroup SpectralGroup
      57             :  * \brief Compute the relative truncation error.
      58             :  *
      59             :  * The negative logarithm of this quantity is defined by Eqs. (57) and
      60             :  * (58) of Ref. \cite Szilagyi2014fna, i.e.,
      61             :  *
      62             :  * \f{align*}{
      63             :  *  \mathcal{T}\left[P_k\right] = \log_{10} \max \left(P_0, P_1\right)
      64             :  *   - \dfrac{\sum_{j=0}^{j_{\text{max}, k}} \log_{10} \left(P_j\right) w_j}
      65             :  *   {\sum_{j=0}^{j_{\text{max}, k}} w_j} , \f}
      66             :  *
      67             :  * with weights
      68             :  *
      69             :  * \f{align*}{
      70             :  *  w_j = \exp\left[ - \left(j - j_{\text{max}, k}
      71             :  *          + \dfrac{1}{2}\right)^2 \right] .
      72             :  * \f}
      73             :  *
      74             :  * where \f$ j_{\text{max}, k}  = N_k - 1 \f$ and  \f$ N_k \f$ is the number of
      75             :  * modes or gridpoints in dimension k. Here the second term is a weighted
      76             :  * average with larger weights toward the highest modes.
      77             :  *
      78             :  * \note Modes below a cutoff of $100 \epsilon \mathrm{max}_k(P_k)$ are ignored
      79             :  * in the weighted average, where $\epsilon$ is the machine epsilon. This
      80             :  * ensures that we don't underestimate the truncation error if some modes are
      81             :  * zero (e.g. by symmetry). Furthermore, if the last two or more modes are zero,
      82             :  * we assume that the function is represented exactly and return a relative
      83             :  * truncation error of zero.
      84             :  *
      85             :  * \details The number of modes (`num_modes_to_use`) argument needs to be less
      86             :  * or equal than the total number of power monitors (`power_monitor.size()`).
      87             :  * In contrast with Ref. \cite Szilagyi2014fna, here we index the modes starting
      88             :  * from zero.
      89             :  *
      90             :  */
      91           1 : double relative_truncation_error(const DataVector& power_monitor,
      92             :                                  size_t num_modes_to_use);
      93             : /// @}
      94             : 
      95             : /*!
      96             :  * \brief The relative truncation error in each logical direction of the grid
      97             :  *
      98             :  * This overload is intended for visualization purposes only. It takes a tensor
      99             :  * component as input, so it can be used as a kernel to post-process volume data
     100             :  * with Python bindings (see `TransformVolumeData.py`).
     101             :  */
     102             : template <typename VectorType, size_t Dim>
     103           1 : std::array<double, Dim> relative_truncation_error(
     104             :     const VectorType& tensor_component, const Mesh<Dim>& mesh);
     105             : 
     106             : /// @{
     107             : /*!
     108             :  * \ingroup SpectralGroup
     109             :  * \brief Returns an estimate of the absolute truncation error in each
     110             :  * dimension.
     111             :  *
     112             :  * The estimate of the numerical error is given by
     113             :  *
     114             :  * \f{align*}{
     115             :  *  \mathcal{E}\left[P_k\right] = u_\mathrm{max} \times 10^{- \mathcal{T}[P_k]},
     116             :  * \f}
     117             :  *
     118             :  * where \f$ u_\mathrm{max} = \mathrm{max} |u|\f$ in the corresponding element
     119             :  * and \f$ \mathcal{T}[P_k] \f$ is the relative error estimate
     120             :  * computed from the power monitors \f$ P_k \f$.
     121             :  *
     122             :  * \warning This estimate is intended for visualization purposes only.
     123             :  */
     124             : template <typename VectorType, size_t Dim>
     125           1 : std::array<double, Dim> absolute_truncation_error(
     126             :     const VectorType& tensor_component, const Mesh<Dim>& mesh);
     127             : /// @}
     128             : 
     129             : /// Holds convergence rate and pile up modes of a power monitor
     130           1 : struct ConvergenceInfo {
     131           0 :   double convergence_rate{std::numeric_limits<double>::signaling_NaN()};
     132           0 :   double number_of_pile_up_modes{std::numeric_limits<double>::signaling_NaN()};
     133             : };
     134             : 
     135             : /*!
     136             :  * \ingroup SpectralGroup
     137             :  * \brief Returns the convergence rate and the number of pile up modes of a
     138             :  * power monitor as a ConvergenceInfo.
     139             :  *
     140             :  * \details Computes the convergence rate of a power monitor as a weighted
     141             :  * average of slopes measured using different subsets of spectral modes
     142             :  * in the power monitor. Equation (53) of \cite Szilagyi2014fna gives
     143             :  * the convergence rate $\mathcal{C}$ in terms of a power monitor $P_k$ as
     144             :  * \begin{equation}
     145             :  * \mathcal{C}(P_k) = -\frac{\sum_{k_1=0}^2\sum_{k_2=\tilde{k}_1}^{\tilde{N}-1}
     146             :  *   \frac{\mathcal{S}(k_1,k_2)}{\epsilon + \mathcal{E}(k_1,k_2)}}{
     147             :  *   \sum_{k_1=0}^2\sum_{k_2=\tilde{k}_1}^{\tilde{N}-1}
     148             :  *   \frac{1}{\epsilon + \mathcal{E}(k_1,k_2)}}.
     149             :  * \end{equation}
     150             :  * Here, $\mathcal{S}(k_1,k_2)$ is the slope of a linear regression fit of
     151             :  * $\log_{10}(P_k)$ with $k$ satisfying $k_1\leq k \leq k_2$,
     152             :  * $\mathcal{E}(k_1,k_2)$ is the error of the slope in that fit,
     153             :  * $\epsilon=\max\left(10^{-3}\max\left(\mathcal{E}(k_1,k_2)\right),
     154             :  * 10^{-15}\right)$ is a small number to avoid dividing by zero in the event the
     155             :  * fit errors vanish,
     156             :  * $\max\left(\mathcal{E}(k_1,k_2)\right)$ is the maximum fit error of each
     157             :  * fit whose slope is included in the summation,
     158             :  * $\tilde{k}_1 = \min\left(k_1+4,\tilde{N}-1\right)$,
     159             :  * $\tilde{N} = N-N_f$, $N$ is the number of modes in the
     160             :  * power monitor, and the highest $N_f$ modes are filtered. Note that the
     161             :  * way $\epsilon$ is defined is so that it matches SpEC's definition, while
     162             :  * also ensuring that it is nonzero even if the error in the slope fit is
     163             :  * exactly zero.
     164             :  *
     165             :  * Also computes the number of pile up modes in a power monitor. Pile up
     166             :  * modes are modes where the power is no longer converging at the overall
     167             :  * convergence rate. Following Eq. (56) of \cite Szilagyi2014fna, the number of
     168             :  * pile up modes $\mathcal{P}$ is defined as
     169             :  * \begin{equation}
     170             :  * \mathcal{P}(P_k) = \sum_{j=2}^{\tilde{N}-2}
     171             :  * \exp\left[-32\left(\frac{\tilde{\mathcal{C}}_j}
     172             :  * {\mathcal{C}(P_k)}\right)^2\right],
     173             :  * \end{equation}
     174             :  * where $\mathcal{C}(P_k)$ is the convergence rate of the power monitor $P_k$,
     175             :  * the local convergence rate $\tilde{\mathcal{C}}_j$ of mode $j$ is
     176             :  * \begin{equation}
     177             :  * \tilde{\mathcal{C}}_j = -\mathcal{S}(j,\min(\tilde{N}-1,j+4)),
     178             :  * \end{equation}
     179             :  * $\mathcal{S}(k_1,k_2)$ is the slope of a linear regression fit of
     180             :  * $\log_{10}(P_k)$ with $k$ satisfying $k_1\leq k \leq k_2$,
     181             :  * $\tilde{N} = N-N_f$, $N$ is the number of modes in the
     182             :  * power monitor, and the highest $N_f$ modes are filtered.
     183             :  * The motivation of this definition is the following: if the local
     184             :  * convergence rate $\tilde{\mathcal{C}}_j$ is comparable to the overall
     185             :  * convergence rate $\mathcal{C}(P_k)$, then the $j^{\rm th}$ term in the
     186             :  * summation becomes $\approx \exp(-32) \approx 10^{-14}$, while if
     187             :  * $\tilde{\mathcal{C}}_j \ll \mathcal{C}(P_k)$, then the $j^{\rm th}$ term in
     188             :  * the summation is $\approx \exp(0) = 1$. Note that the coefficient value 32
     189             :  * is chosen to agree with SpEC.
     190             :  * \note The summation goes up to $\tilde{N}-2$ so that there is it least one
     191             :  * larger unfiltered mode for use in computing the slope. The highest mode
     192             :  * used when computing the slope is the highest unfiltered mode, $\tilde{N}-1$.
     193             :  * Mode numbers in the power monitor are zero based. These choices are off by
     194             :  * one vs. Eqs. (55) and (56) of \cite Szilagyi2014fna, because those formulas
     195             :  * apparently assume one-based indexing.
     196             :  * \param power_monitor The power monitor.
     197             :  * \param number_of_filtered_modes How many of the highest modes of the
     198             :  * power monitor are filtered (default 0).
     199             :  */
     200           1 : ConvergenceInfo convergence_rate_and_number_of_pile_up_modes(
     201             :     const DataVector& power_monitor, size_t number_of_filtered_modes = 0);
     202             : 
     203             : /// @{
     204             : /*!
     205             :  * \brief Return the radial power monitor for a tensor component on a
     206             :  * spherical shell.
     207             :  *
     208             :  * The mesh dimensions are assumed to be ordered `(radial, theta, phi)`. The
     209             :  * radial grid points are contiguous, so each angular point supplies one
     210             :  * radial slice to the one-dimensional modal transform.
     211             :  */
     212           1 : void spherical_shell_radial_power_monitor(gsl::not_null<DataVector*> result,
     213             :                                           const DataVector& tensor_component,
     214             :                                           const Mesh<3>& mesh);
     215             : 
     216           1 : DataVector spherical_shell_radial_power_monitor(
     217             :     const DataVector& tensor_component, const Mesh<3>& mesh);
     218             : /// @}
     219             : 
     220             : /// @{
     221             : /*!
     222             :  * \brief Return the angular power monitor for one TensorYlm component on a
     223             :  * spherical shell.
     224             :  *
     225             :  * The mesh dimensions are assumed to be ordered `(radial, theta, phi)`, with
     226             :  * `l_max == m_max`. TensorYlm coefficients use the radial dimension as the
     227             :  * fastest-moving extent. As reviewed in Sec. II of \cite Boyle2023,
     228             :  * spin-weighted spherical harmonics with `l < |spin_weight|` vanish, so these
     229             :  * modes are omitted from both the sum and its normalization. Set
     230             :  * `zero_m_is_real` for real scalar coefficients, which have no imaginary
     231             :  * `m=0` coefficients in Spherepack storage.
     232             :  */
     233           1 : void spherical_shell_angular_power_monitor(
     234             :     gsl::not_null<DataVector*> result, const DataVector& tensor_ylm_component,
     235             :     const Mesh<3>& mesh, int spin_weight, bool zero_m_is_real);
     236             : 
     237           1 : DataVector spherical_shell_angular_power_monitor(
     238             :     const DataVector& tensor_ylm_component, const Mesh<3>& mesh,
     239             :     int spin_weight, bool zero_m_is_real);
     240             : /// @}
     241             : 
     242             : /*!
     243             :  * \brief Return the RMS radial power monitor across all components of a
     244             :  * tensor on a spherical shell.
     245             :  *
     246             :  * Combines `spherical_shell_radial_power_monitor` for each of
     247             :  * `tensor.size()` components in quadrature, normalized by the number of
     248             :  * components.
     249             :  */
     250             : template <typename TensorType>
     251           1 : DataVector spherical_shell_tensor_radial_power_monitor(const TensorType& tensor,
     252             :                                                        const Mesh<3>& mesh) {
     253             :   DataVector squared_power(mesh.extents(0), 0.0);
     254             :   DataVector component_power{};
     255             :   for (size_t component = 0; component < tensor.size(); ++component) {
     256             :     spherical_shell_radial_power_monitor(make_not_null(&component_power),
     257             :                                          tensor[component], mesh);
     258             :     squared_power += square(component_power);
     259             :   }
     260             :   squared_power = sqrt(squared_power / static_cast<double>(tensor.size()));
     261             :   return squared_power;
     262             : }
     263             : 
     264             : /*!
     265             :  * \brief Number of independent TensorYlm coefficients contributing to
     266             :  * angular degree `ell`, summed over `radial_extents` radial points.
     267             :  *
     268             :  * Returns 0 when `ell < |spin_weight|`, since spin-weighted spherical
     269             :  * harmonics vanish there (such terms are then excluded from
     270             :  * `accumulate_spherical_shell_tensor_angular_power()`'s sum and normalization).
     271             :  * See `SpherepackIterator` for the meaning of `zero_m_is_real`.
     272             :  */
     273           1 : size_t spherical_shell_number_of_angular_coefficients(size_t ell,
     274             :                                                       int spin_weight,
     275             :                                                       bool zero_m_is_real,
     276             :                                                       size_t radial_extents);
     277             : 
     278             : /*!
     279             :  * \brief Accumulate weighted-squared angular TensorYlm power and mode counts
     280             :  * for all components of a tensor on a spherical shell.
     281             :  *
     282             :  * Adds to `weighted_squared_power` and `counts` in place, so this can be
     283             :  * called repeatedly to combine several tensors into the same angular power
     284             :  * monitor before calling `normalize_spherical_shell_angular_power`.
     285             :  */
     286             : template <typename TensorType>
     287           1 : void accumulate_spherical_shell_tensor_angular_power(
     288             :     const gsl::not_null<DataVector*> weighted_squared_power,
     289             :     const gsl::not_null<std::vector<size_t>*> counts, const TensorType& tensor,
     290             :     const Mesh<3>& mesh) {
     291             :   const size_t radial_extents = mesh.extents(0);
     292             :   const size_t ell_max = mesh.extents(1) - 1;
     293             :   constexpr bool zero_m_is_real = TensorType::rank() == 0;
     294             :   DataVector component_power{};
     295             :   for (size_t component = 0; component < tensor.size(); ++component) {
     296             :     const int spin_weight = ylm::TensorYlm::helpers::component_spin_weight<
     297             :         typename TensorType::structure>(component);
     298             :     spherical_shell_angular_power_monitor(make_not_null(&component_power),
     299             :                                           tensor[component], mesh, spin_weight,
     300             :                                           zero_m_is_real);
     301             :     for (size_t ell = 0; ell <= ell_max; ++ell) {
     302             :       const size_t component_count =
     303             :           spherical_shell_number_of_angular_coefficients(
     304             :               ell, spin_weight, zero_m_is_real, radial_extents);
     305             :       (*weighted_squared_power)[ell] +=
     306             :           static_cast<double>(component_count) * square(component_power[ell]);
     307             :       (*counts)[ell] += component_count;
     308             :     }
     309             :   }
     310             : }
     311             : 
     312             : /*!
     313             :  * \brief Normalize an angular power monitor accumulator in place.
     314             :  *
     315             :  * Sets `power[ell] = sqrt(power[ell] / counts[ell])`, or 0 when
     316             :  * `counts[ell] == 0`.
     317             :  */
     318           1 : void normalize_spherical_shell_angular_power(gsl::not_null<DataVector*> power,
     319             :                                              const std::vector<size_t>& counts);
     320             : 
     321             : /*!
     322             :  * \brief Accumulate squared ZernikeB3 Jacobi spectral coefficients for one
     323             :  * TensorYlm component into radial and angular power bins.
     324             :  *
     325             :  * `spec_buf` has layout `spec_buf[s * n_r + i_r]` where `s` is the SPHEREPACK
     326             :  * offset and `i_r` is the radial collocation index. Modes are binned radially
     327             :  * via `radial_mode = (n_total + 1) / 2` where `n_total = l + 2 * k_spec`, and
     328             :  * angularly by degree \f$\ell\f$. Modes with `l < |spin_weight|` are skipped.
     329             :  *
     330             :  * `offsets_by_l` must be pre-computed for the correct `zero_m_is_real` value
     331             :  * of this component. `gathered` and `modal_buf` are caller-owned scratch
     332             :  * buffers of size `max_n_modes_l * n_r` each, where
     333             :  * `max_n_modes_l = 2 * (l_max + 1)`.
     334             :  */
     335           1 : void accumulate_b3_tensor_component_sums(
     336             :     gsl::not_null<DataVector*> sum_sq_radial,
     337             :     gsl::not_null<DataVector*> counts_radial,
     338             :     gsl::not_null<DataVector*> sum_sq_angular,
     339             :     gsl::not_null<DataVector*> counts_angular, const double* spec_buf,
     340             :     size_t n_r, size_t n_r_max, int spin_weight,
     341             :     const std::vector<std::vector<size_t>>& offsets_by_l, double* gathered,
     342             :     double* modal_buf);
     343             : 
     344             : /*!
     345             :  * \brief Accumulate squared ZernikeB3 spectral coefficients for all components
     346             :  * of a TensorYlm tensor into radial and angular power bins.
     347             :  *
     348             :  * Selects `offsets_by_l_real` for rank-0 (scalar) tensors and
     349             :  * `offsets_by_l_complex` for all higher-rank tensors. The spin weight for each
     350             :  * component is determined from the tensor structure.
     351             :  */
     352             : template <typename TensorType>
     353           1 : void accumulate_b3_tensor_sums(
     354             :     gsl::not_null<DataVector*> sum_sq_radial,
     355             :     gsl::not_null<DataVector*> counts_radial,
     356             :     gsl::not_null<DataVector*> sum_sq_angular,
     357             :     gsl::not_null<DataVector*> counts_angular, const TensorType& tensor,
     358             :     size_t n_r, size_t n_r_max,
     359             :     const std::vector<std::vector<size_t>>& offsets_by_l_real,
     360             :     const std::vector<std::vector<size_t>>& offsets_by_l_complex,
     361             :     double* gathered, double* modal_buf) {
     362             :   constexpr bool zero_m_is_real = TensorType::rank() == 0;
     363             :   const auto& offsets_by_l =
     364             :       zero_m_is_real ? offsets_by_l_real : offsets_by_l_complex;
     365             :   for (size_t component = 0; component < tensor.size(); ++component) {
     366             :     const int spin_weight = ylm::TensorYlm::helpers::component_spin_weight<
     367             :         typename TensorType::structure>(component);
     368             :     accumulate_b3_tensor_component_sums(
     369             :         sum_sq_radial, counts_radial, sum_sq_angular, counts_angular,
     370             :         tensor[component].data(), n_r, n_r_max, spin_weight, offsets_by_l,
     371             :         gathered, modal_buf);
     372             :   }
     373             : }
     374             : 
     375             : /*!
     376             :  * \brief Normalize a B3 power monitor accumulator in place.
     377             :  *
     378             :  * Sets `result[i] = sqrt(sum_sq[i] / counts[i])`, or 0 when `counts[i] == 0`.
     379             :  */
     380           1 : void normalize_b3_power(gsl::not_null<DataVector*> result,
     381             :                         const DataVector& sum_sq, const DataVector& counts);
     382             : 
     383             : }  // namespace PowerMonitors

Generated by: LCOV version 1.14