SpECTRE Documentation Coverage Report
Current view: top level - NumericalAlgorithms/LinearOperators - PowerMonitors.hpp Coverage Total Hit
Commit: 0d1d717af84a81fb1284a20c4013708fe85cc2f0 Lines: 86.4 % 22 19
Test Date: 2026-10-22 17:29:42
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 2.0-1