SpECTRE Documentation Coverage Report
Current view: top level - ControlSystem/ControlErrors - Skew.hpp Hit Total Coverage
Commit: 1f2210958b4f38fdc0400907ee7c6d5af5111418 Lines: 3 20 15.0 %
Date: 2025-12-05 05:03:31
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 "Domain/Creators/Tags/ObjectCenter.hpp"
      18             : #include "Domain/Structure/ObjectLabel.hpp"
      19             : #include "IO/Logging/Verbosity.hpp"
      20             : #include "NumericalAlgorithms/SphericalHarmonics/Spherepack.hpp"
      21             : #include "NumericalAlgorithms/SphericalHarmonics/Strahlkorper.hpp"
      22             : #include "NumericalAlgorithms/SphericalHarmonics/Tags.hpp"
      23             : #include "Options/String.hpp"
      24             : #include "Parallel/GlobalCache.hpp"
      25             : #include "Utilities/Algorithm.hpp"
      26             : #include "Utilities/ProtocolHelpers.hpp"
      27             : #include "Utilities/TMPL.hpp"
      28             : #include "Utilities/TaggedTuple.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             :                       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 1.14