LCOV - code coverage report
Current view: top level - cell - ReactionEngine.cpp (source / functions) Hit Total Coverage
Test: coverage.info Lines: 102 112 91.1 %
Date: 2025-12-26 22:55:38 Functions: 12 12 100.0 %

          Line data    Source code
       1             : #include "ReactionEngine.hpp"
       2             : #include "Disc.hpp"
       3             : #include "MathUtils.hpp"
       4             : #include "Reaction.hpp"
       5             : #include "ReactionTable.hpp"
       6             : 
       7             : namespace cell
       8             : {
       9             : 
      10          11 : ReactionEngine::ReactionEngine(const DiscTypeRegistry& discTypeRegistry, const ReactionTable& reactionTable)
      11          11 :     : discTypeRegistry_(discTypeRegistry)
      12             : {
      13          11 :     combineReactionsIntoSingleMaps(reactionTable);
      14          11 : }
      15             : 
      16           1 : Disc ReactionEngine::transformationReaction(Disc* educt, DiscTypeID productID) const
      17             : {
      18           1 :     Disc product(*educt);
      19           1 :     product.setType(productID);
      20           1 :     educt->markDestroyed();
      21             : 
      22           1 :     return product;
      23             : }
      24             : 
      25           1 : std::pair<Disc, Disc> ReactionEngine::decompositionReaction(Disc* educt, DiscTypeID product1ID,
      26             :                                                             DiscTypeID product2ID) const
      27             : {
      28           1 :     double v = mathutils::abs(educt->getVelocity());
      29           1 :     if (v == 0)
      30             :     {
      31             :         // If the disc is stationary and wants to split apart, we'll give it a random velocity to do so
      32           0 :         educt->setVelocity(
      33           0 :             Vector2d{mathutils::getRandomNumber<double>(-10, 10), mathutils::getRandomNumber<double>(-10, 10)});
      34           0 :         v = mathutils::abs(educt->getVelocity());
      35             :     }
      36             : 
      37           1 :     const Vector2d eductNormalizedVelocity = educt->getVelocity() / v;
      38           1 :     const Vector2d n{-eductNormalizedVelocity.y, eductNormalizedVelocity.x};
      39             : 
      40           1 :     Disc product1(*educt);
      41           1 :     Disc product2(product2ID);
      42             : 
      43           1 :     product1.setType(product1ID);
      44           1 :     product1.setVelocity(v * n);
      45             : 
      46           1 :     product2.setPosition(educt->getPosition());
      47           1 :     product2.setVelocity(-v * n);
      48             : 
      49           1 :     const auto R1 = discTypeRegistry_.getByID(product1.getTypeID()).getRadius();
      50           1 :     const auto R2 = discTypeRegistry_.getByID(product2.getTypeID()).getRadius();
      51           1 :     const auto overlap = R1 + R2 + 1e-6; // Discs at same position always have maximum overlap R1 + R2
      52             : 
      53           1 :     product1.move(0.5 * overlap * n);
      54           1 :     product2.move(-0.5 * overlap * n);
      55             : 
      56           1 :     educt->markDestroyed();
      57             : 
      58           2 :     return std::make_pair(std::move(product1), std::move(product2));
      59             : }
      60             : 
      61           5 : Disc ReactionEngine::combinationReaction(Disc* educt1, Disc* educt2, DiscTypeID productID) const
      62             : {
      63           5 :     const auto& resultType = discTypeRegistry_.getByID(productID);
      64           5 :     const auto& d1Type = discTypeRegistry_.getByID(educt1->getTypeID());
      65           5 :     const auto& d2Type = discTypeRegistry_.getByID(educt2->getTypeID());
      66             : 
      67           5 :     Disc newDisc(productID);
      68          10 :     newDisc.setVelocity((d1Type.getMass() * educt1->getVelocity() + d2Type.getMass() * educt2->getVelocity()) /
      69          10 :                         resultType.getMass());
      70           5 :     newDisc.setPosition((educt1->getPosition() + educt2->getPosition()) / 2.0);
      71             : 
      72           5 :     educt1->markDestroyed();
      73           5 :     educt2->markDestroyed();
      74             : 
      75           5 :     return newDisc;
      76             : }
      77             : 
      78           1 : std::pair<Disc, Disc> ReactionEngine::exchangeReaction(Disc* educt1, Disc* educt2, DiscTypeID product1ID,
      79             :                                                        DiscTypeID product2ID) const
      80             : {
      81           1 :     const auto* d1Type = &discTypeRegistry_.getByID(educt1->getTypeID());
      82           1 :     const auto* d2Type = &discTypeRegistry_.getByID(educt2->getTypeID());
      83           1 :     const auto* product1Type = &discTypeRegistry_.getByID(product1ID);
      84           1 :     const auto* product2Type = &discTypeRegistry_.getByID(product2ID);
      85             : 
      86             :     // Sort both product types and educt discs by radius
      87             :     // Now the smallest/largest disc gets the smallest/largest product type
      88             : 
      89           1 :     if (product1Type->getRadius() > product2Type->getRadius())
      90             :     {
      91           0 :         std::swap(product1Type, product2Type);
      92           0 :         std::swap(product1ID, product2ID);
      93             :     }
      94           1 :     if (d1Type->getRadius() > d2Type->getRadius())
      95             :     {
      96           0 :         std::swap(educt1, educt2);
      97           0 :         std::swap(d1Type, d2Type);
      98             :     }
      99             : 
     100             :     // Prefer the assignment that keeps
     101             :     // as many discs as possible with their original type.
     102             : 
     103           1 :     int leaveAsIs = (educt1->getTypeID() == product1ID) + (educt2->getTypeID() == product2ID);
     104           1 :     int swapAgain = (educt1->getTypeID() == product2ID) + (educt2->getTypeID() == product1ID);
     105             : 
     106           1 :     if (swapAgain > leaveAsIs)
     107             :     {
     108           0 :         std::swap(product1Type, product2Type);
     109           0 :         std::swap(product1ID, product2ID);
     110             :     }
     111             : 
     112           1 :     Disc product1(*educt1);
     113           1 :     Disc product2(*educt2);
     114             : 
     115           1 :     product1.scaleVelocity(std::sqrt(d1Type->getMass() / product1Type->getMass()));
     116           1 :     product1.setType(product1ID);
     117             : 
     118           1 :     product2.scaleVelocity(std::sqrt(d2Type->getMass() / product2Type->getMass()));
     119           1 :     product2.setType(product2ID);
     120             : 
     121           1 :     educt1->markDestroyed();
     122           1 :     educt2->markDestroyed();
     123             : 
     124           2 :     return std::make_pair(std::move(product1), std::move(product2));
     125             : }
     126             : 
     127          26 : void ReactionEngine::applyUnimolecularReactions(Disc& disc, double dt, std::vector<Disc>& newDiscs) const
     128             : {
     129          26 :     const Reaction* reaction = selectUnimolecularReaction(disc.getTypeID(), dt);
     130          26 :     if (!reaction)
     131          24 :         return;
     132             : 
     133           2 :     if (reaction->getType() == Reaction::Type::Transformation)
     134             :     {
     135           1 :         auto product = transformationReaction(&disc, reaction->getProduct1());
     136           1 :         newDiscs.push_back(std::move(product));
     137             :     }
     138             :     else
     139             :     {
     140           1 :         auto products = decompositionReaction(&disc, reaction->getProduct1(), reaction->getProduct2());
     141           1 :         newDiscs.push_back(std::move(products.first));
     142           1 :         newDiscs.push_back(std::move(products.second));
     143             :     }
     144             : }
     145             : 
     146          13 : void ReactionEngine::applyBimolecularReactions(const std::vector<CollisionDetector::Collision>& collisions,
     147             :                                                std::vector<Disc>& newDiscs) const
     148             : {
     149          20 :     for (const auto& collision : collisions)
     150             :     {
     151           7 :         if (collision.invalidatedByDestroyedDiscs())
     152           0 :             continue;
     153             : 
     154             :         const Reaction* reaction =
     155           7 :             selectBimolecularReaction(std::minmax(collision.disc->getTypeID(), collision.otherDisc->getTypeID()));
     156           7 :         if (!reaction)
     157           1 :             continue;
     158             : 
     159           6 :         if (reaction->getType() == Reaction::Type::Combination)
     160             :         {
     161           5 :             auto product = combinationReaction(collision.disc, collision.otherDisc, reaction->getProduct1());
     162           5 :             newDiscs.push_back(std::move(product));
     163             :         }
     164             :         else
     165             :         {
     166             :             auto products =
     167           1 :                 exchangeReaction(collision.disc, collision.otherDisc, reaction->getProduct1(), reaction->getProduct2());
     168           1 :             newDiscs.push_back(std::move(products.first));
     169           1 :             newDiscs.push_back(std::move(products.second));
     170             :         }
     171             :     }
     172          13 : }
     173             : 
     174          26 : const Reaction* ReactionEngine::selectUnimolecularReaction(const DiscTypeID& key, double dt) const
     175             : {
     176          26 :     return selectReaction(
     177          26 :         unimolecularReactions_, key, [&](const Reaction& reaction)
     178          54 :         { return mathutils::getRandomNumber<double>(0, 1) <= 1 - std::pow(1 - reaction.getProbability(), dt); });
     179             : }
     180             : 
     181           7 : const Reaction* ReactionEngine::selectBimolecularReaction(const std::pair<DiscTypeID, DiscTypeID>& key) const
     182             : {
     183          14 :     return selectReaction(bimolecularReactions_, key, [](const Reaction& reaction)
     184          20 :                           { return mathutils::getRandomNumber<double>(0, 1) <= reaction.getProbability(); });
     185             : }
     186             : 
     187          11 : void ReactionEngine::combineReactionsIntoSingleMaps(const ReactionTable& reactionTable)
     188             : {
     189          55 :     for (const auto& table : {reactionTable.getTransformations(), reactionTable.getDecompositions()})
     190             :     {
     191          24 :         for (const auto& [educt, reactions] : table)
     192           2 :             unimolecularReactions_[educt].insert(unimolecularReactions_[educt].end(), reactions.begin(),
     193             :                                                  reactions.end());
     194          33 :     }
     195             : 
     196          55 :     for (const auto& table : {reactionTable.getCombinations(), reactionTable.getExchanges()})
     197             :     {
     198          27 :         for (const auto& [educts, reactions] : table)
     199           5 :             bimolecularReactions_[educts].insert(bimolecularReactions_[educts].end(), reactions.begin(),
     200             :                                                  reactions.end());
     201          33 :     }
     202             : 
     203             :     // Random shuffle all reactions to avoid a bias
     204          11 :     std::minstd_rand rng{std::random_device{}()};
     205             : 
     206          13 :     for (auto& [educt, reactions] : unimolecularReactions_)
     207           2 :         std::shuffle(reactions.begin(), reactions.end(), rng);
     208             : 
     209          16 :     for (auto& [educts, reactions] : bimolecularReactions_)
     210           5 :         std::shuffle(reactions.begin(), reactions.end(), rng);
     211          11 : }
     212             : 
     213             : } // namespace cell

Generated by: LCOV version 1.14