SpECTRE Documentation Coverage Report
Current view: top level - ControlSystem/ControlErrors - Skew.hpp Coverage Total Hit
Commit: 0d1d717af84a81fb1284a20c4013708fe85cc2f0 Lines: 15.0 % 20 3
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 <limits>
       7              : #include <optional>
       8              : #include <pup.h>
       9              : #include <string>
      10              : #include <utility>
      11              : 
      12              : #include "ControlSystem/Protocols/ControlError.hpp"
      13              : #include "ControlSystem/Tags/QueueTags.hpp"
      14              : #include "ControlSystem/TimescaleTuner.hpp"
      15              : #include "DataStructures/DataBox/DataBox.hpp"
      16              : #include "DataStructures/DataVector.hpp"
      17              : #include "DataStructures/TaggedTuple.hpp"
      18              : #include "Domain/Creators/Tags/ObjectCenter.hpp"
      19              : #include "Domain/Structure/ObjectLabel.hpp"
      20              : #include "IO/Logging/Verbosity.hpp"
      21              : #include "NumericalAlgorithms/SphericalHarmonics/Spherepack.hpp"
      22              : #include "NumericalAlgorithms/Strahlkorper/Strahlkorper.hpp"
      23              : #include "NumericalAlgorithms/Strahlkorper/Tags.hpp"
      24              : #include "Options/String.hpp"
      25              : #include "Parallel/GlobalCache.hpp"
      26              : #include "Utilities/Algorithm.hpp"
      27              : #include "Utilities/ProtocolHelpers.hpp"
      28              : #include "Utilities/TMPL.hpp"
      29              : 
      30              : /// \cond
      31              : namespace domain::Tags {
      32              : struct FunctionsOfTime;
      33              : }  // namespace domain::Tags
      34              : namespace Frame {
      35              : struct Grid;
      36              : struct Distorted;
      37              : }  // namespace Frame
      38              : /// \endcond
      39              : 
      40              : namespace control_system::ControlErrors {
      41              : /*!
      42              :  * \brief Control error in the for the
      43              :  * `domain::CoordinateMaps::TimeDependent::Skew` map.
      44              :  *
      45              :  * \details Computes the error in the map parameters $F_y(t)$ and $F_z(t)$ of
      46              :  * the \link domain::CoordinateMaps::TimeDependent::Skew Skew \endlink using
      47              :  * a modified version of Eq. (71) from \cite Hemberger2012jz. This modified
      48              :  * control error is
      49              :  *
      50              :  * \begin{equation}
      51              :  * Q_{F_j} = g\left(\frac{w_A \Theta_A^j + w_B \Theta_B^j}{w_A + w_B}\right)
      52              :  *     - (1-g)F_j
      53              :  * \end{equation}
      54              :  *
      55              :  * where the falloff function is assumed to be $W(\vec{x}) \approx 1$ from the
      56              :  * \link domain::CoordinateMaps::TimeDependent::Skew Skew \endlink map,
      57              :  * $\Theta_H^j$ are the inclination angles between the $x$-axis and the normal
      58              :  * to the horizon at the intersection point $x_H^\textrm{Int}$ between the
      59              :  * $x$-axis and the horizon, $w_A$ and $w_B$ are averaging weights defined by
      60              :  *
      61              :  * \begin{equation}
      62              :  * w_H = \exp{\left(-\frac{x^0_C - x_H^\textrm{Int}}{x^0_C - C^0_H}\right)}
      63              :  * \end{equation}
      64              :  *
      65              :  * with $C^0_H$ being the centers of the excision boundaries, and finally with
      66              :  *
      67              :  * \begin{equation}
      68              :  * g = \frac{1}{2}\left(1-\tanh\left(10\frac{x_A^\textrm{Int} -
      69              :  * x_B^\textrm{Int}}{C^0_A - C^0_B} - 5\right)\right).
      70              :  * \end{equation}
      71              :  *
      72              :  * This transition function $g$ is meant to only activate skew control when the
      73              :  * black holes are close to merger to avoid adverse effects with junk radiation.
      74              :  * If $g < 0.0025$, then we turn skew control off completely and the control
      75              :  * error just becomes
      76              :  *
      77              :  * \begin{equation}
      78              :  * Q_{F_j} = -F_j.
      79              :  * \end{equation}
      80              :  *
      81              :  * This threshold value of $g = 0.0025$ was chosen in SpEC and seems to work
      82              :  * well.
      83              :  *
      84              :  * Requirements:
      85              :  * - This control error requires that there be exactly two objects in the
      86              :  *   simulation
      87              :  * - Currently both these objects must be black holes
      88              :  * - Currently this control system can only be used with the \link
      89              :  *   control_system::Systems::Skew Skew \endlink control system
      90              :  */
      91            1 : struct Skew : tt::ConformsTo<protocols::ControlError> {
      92            0 :   using object_centers = domain::object_list<>;
      93              : 
      94            0 :   using options = tmpl::list<>;
      95            0 :   static constexpr Options::String help{
      96              :       "Computes the control error for skew control."};
      97              : 
      98              :   // Explicitly defined copy constructor because DataBox isn't copyable
      99            0 :   Skew() = default;
     100            0 :   Skew(Skew&& rhs) = default;
     101            0 :   Skew& operator=(Skew&& rhs) = default;
     102            0 :   Skew(const Skew&);
     103            0 :   Skew& operator=(const Skew&);
     104            0 :   ~Skew() = default;
     105              : 
     106              :   /*!
     107              :    * \brief Returns the internal suggested timescale. A std::nullopt means that
     108              :    * no timescale is suggested.
     109              :    */
     110            1 :   std::optional<double> get_suggested_timescale() const;
     111              : 
     112              :   /*!
     113              :    * \brief Resets the internal suggested timescale to nullopt.
     114              :    */
     115            1 :   void reset();
     116              : 
     117            0 :   void pup(PUP::er& p);
     118              : 
     119              :   template <typename Metavariables, typename... TupleTags>
     120            0 :   DataVector operator()(const ::TimescaleTuner<true>& /*tuner*/,
     121              :                         const Parallel::GlobalCache<Metavariables>& cache,
     122              :                         const double time,
     123              :                         const std::string& function_of_time_name,
     124              :                         const tuples::TaggedTuple<TupleTags...>& measurements) {
     125              :     const ylm::Strahlkorper<Frame::Distorted>& horizon_a = tuples::get<
     126              :         QueueTags::Horizon<Frame::Distorted, ::domain::ObjectLabel::A>>(
     127              :         measurements);
     128              :     const ylm::Strahlkorper<Frame::Distorted>& horizon_b = tuples::get<
     129              :         QueueTags::Horizon<Frame::Distorted, ::domain::ObjectLabel::B>>(
     130              :         measurements);
     131              : 
     132              :     // Copy the horizon into the box so the compute tags use the correct
     133              :     // strahlkorper
     134              :     const auto set_horizon =
     135              :         [](const gsl::not_null<ylm::Strahlkorper<Frame::Distorted>*>
     136              :                horizon_ptr,
     137              :            const ylm::Strahlkorper<Frame::Distorted>& horizon) {
     138              :           *horizon_ptr = horizon;
     139              :         };
     140              : 
     141              :     db::mutate<ylm::Tags::Strahlkorper<Frame::Distorted>>(
     142              :         set_horizon, make_not_null(&box_a_), horizon_a);
     143              :     db::mutate<ylm::Tags::Strahlkorper<Frame::Distorted>>(
     144              :         set_horizon, make_not_null(&box_b_), horizon_b);
     145              : 
     146              :     const auto& normal_one_form_a =
     147              :         db::get<ylm::Tags::NormalOneForm<Frame::Distorted>>(box_a_);
     148              :     const auto& cartesian_coords_a =
     149              :         db::get<ylm::Tags::CartesianCoords<Frame::Distorted>>(box_a_);
     150              :     const auto& center_a =
     151              :         Parallel::get<domain::Tags::ObjectCenter<domain::ObjectLabel::A>>(
     152              :             cache);
     153              : 
     154              :     const auto& normal_one_form_b =
     155              :         db::get<ylm::Tags::NormalOneForm<Frame::Distorted>>(box_b_);
     156              :     const auto& cartesian_coords_b =
     157              :         db::get<ylm::Tags::CartesianCoords<Frame::Distorted>>(box_b_);
     158              :     const auto& center_b =
     159              :         Parallel::get<domain::Tags::ObjectCenter<domain::ObjectLabel::B>>(
     160              :             cache);
     161              : 
     162              :     // The translation of the cutting plane in the grid frame is just the
     163              :     // average of the two centers.
     164              :     const double cut_x = 0.5 * (center_a[0] + center_b[0]);
     165              : 
     166              :     // For AhA since it always has x_center > cut_x, we want the point which is
     167              :     // closest to the cutting plane, which in terms of theta, phi = pi/2, pi.
     168              :     // For AhB since it always has x_center < cut_x, we want the point which is
     169              :     // closest to the cutting plane, which in terms of theta, phi = pi/2, 0.
     170              :     // To get the coord and normal one form at these theta/phi, we do an
     171              :     // interpolation for both Ah's
     172              :     const auto& ylm_a = horizon_a.ylm_spherepack();
     173              :     const ylm::Spherepack::InterpolationInfo<double> interpolation_info_a =
     174              :         ylm_a.set_up_interpolation_info(std::array{M_PI_2, M_PI});
     175              :     const auto& ylm_b = horizon_b.ylm_spherepack();
     176              :     const ylm::Spherepack::InterpolationInfo<double> interpolation_info_b =
     177              :         ylm_b.set_up_interpolation_info(std::array{M_PI_2, 0.0});
     178              : 
     179              :     const auto set_intersection =
     180              :         [](const gsl::not_null<DataVector*> inclination_angle,
     181              :            const gsl::not_null<std::array<double, 3>*> intersection_coord,
     182              :            const ylm::Spherepack& ylm,
     183              :            const ylm::Spherepack::InterpolationInfo<double>& interpolation_info,
     184              :            const auto& normal_one_form, const auto& cartesian_coords) {
     185              :           std::array<double, 3> intersection_normal_one_form{};
     186              :           for (size_t i = 0; i < 3; i++) {
     187              :             ylm.interpolate(make_not_null(&gsl::at(*intersection_coord, i)),
     188              :                             make_not_null(cartesian_coords.get(i).data()),
     189              :                             interpolation_info);
     190              :             ylm.interpolate(
     191              :                 make_not_null(&gsl::at(intersection_normal_one_form, i)),
     192              :                 make_not_null(normal_one_form.get(i).data()),
     193              :                 interpolation_info);
     194              :           }
     195              : 
     196              :           for (size_t i = 0; i < 2; ++i) {
     197              :             (*inclination_angle)[i] =
     198              :                 atan2(gsl::at(intersection_normal_one_form, i + 1),
     199              :                       (intersection_normal_one_form)[0]);
     200              :             if ((*inclination_angle)[i] < -0.5 * M_PI) {
     201              :               (*inclination_angle)[i] += M_PI;
     202              :             } else if ((*inclination_angle)[i] > +0.5 * M_PI) {
     203              :               (*inclination_angle)[i] -= M_PI;
     204              :             }
     205              :           }
     206              :         };
     207              : 
     208              :     // These are the values we need at the theta/phi described above
     209              :     std::array<double, 3> intersection_coord_a{};
     210              :     std::array<double, 3> intersection_coord_b{};
     211              : 
     212              :     set_intersection(make_not_null(&inclination_angle_a_),
     213              :                      make_not_null(&intersection_coord_a), ylm_a,
     214              :                      interpolation_info_a, normal_one_form_a,
     215              :                      cartesian_coords_a);
     216              :     set_intersection(make_not_null(&inclination_angle_b_),
     217              :                      make_not_null(&intersection_coord_b), ylm_b,
     218              :                      interpolation_info_b, normal_one_form_b,
     219              :                      cartesian_coords_b);
     220              : 
     221              :     const double relative_delta_x =
     222              :         (intersection_coord_a[0] - intersection_coord_b[0]) /
     223              :         (center_a[0] - center_b[0]);
     224              :     // Hardcoded function used in SpEC
     225              :     const double temporal_transition_function =
     226              :         0.5 * (1.0 - tanh(10.0 * relative_delta_x - 5.0));
     227              : 
     228              :     // Hardcoded value used in SpEC
     229              :     constexpr double activation_threshold = 0.0025;
     230              : 
     231              :     const auto& function_of_time =
     232              :         Parallel::get<domain::Tags::FunctionsOfTime>(cache).at(
     233              :             function_of_time_name);
     234              : 
     235              :     // Only activate if BHs are close enough. Otherwise control to zero
     236              :     DataVector func = std::move(function_of_time->func(time)[0]);
     237              :     DataVector& control_error = func;
     238              :     if (temporal_transition_function > activation_threshold) {
     239              :       suggested_timescale_ =
     240              :           std::max(std::min(abs(center_a[0]), abs(center_b[0])),
     241              :                    std::min(abs(cut_x - intersection_coord_a[0]),
     242              :                             abs(cut_x - intersection_coord_b[0])));
     243              : 
     244              :       const double weight_a =
     245              :           exp(-(cut_x - intersection_coord_a[0]) / (cut_x - center_a[0]));
     246              :       const double weight_b =
     247              :           exp(-(cut_x - intersection_coord_b[0]) / (cut_x - center_b[0]));
     248              : 
     249              :       control_error = temporal_transition_function *
     250              :                           (weight_a * inclination_angle_a_ +
     251              :                            weight_b * inclination_angle_b_) /
     252              :                           (weight_a + weight_b) -
     253              :                       (1.0 - temporal_transition_function) * func;
     254              :     } else {
     255              :       control_error *= -1.0;
     256              :     }
     257              : 
     258              :     return std::move(control_error);
     259              :   }
     260              : 
     261              :  private:
     262            0 :   std::optional<double> suggested_timescale_;
     263              :   // not pupped. These are allocated here to avoid memory allocations during
     264              :   // the operator() call. All their data is temporary
     265            0 :   DataVector inclination_angle_a_{2, 0.0};
     266            0 :   DataVector inclination_angle_b_{2, 0.0};
     267              :   db::compute_databox_type<
     268              :       tmpl::append<ylm::Tags::items_tags<Frame::Distorted>,
     269              :                    ylm::Tags::compute_items_tags<Frame::Distorted>>>
     270            0 :       box_a_;
     271              :   db::compute_databox_type<
     272              :       tmpl::append<ylm::Tags::items_tags<Frame::Distorted>,
     273              :                    ylm::Tags::compute_items_tags<Frame::Distorted>>>
     274            0 :       box_b_;
     275              : };
     276              : }  // namespace control_system::ControlErrors
        

Generated by: LCOV version 2.0-1