File indexing completed on 2024-05-19 05:42:16

0001 // ct_lvtqtc_alg_transitive_reduction.cpp                             -*-C++-*-
0002 
0003 /*
0004 // Copyright 2023 Codethink Ltd <codethink@codethink.co.uk>
0005 // SPDX-License-Identifier: Apache-2.0
0006 //
0007 // Licensed under the Apache License, Version 2.0 (the "License");
0008 // you may not use this file except in compliance with the License.
0009 // You may obtain a copy of the License at
0010 //
0011 //     http://www.apache.org/licenses/LICENSE-2.0
0012 //
0013 // Unless required by applicable law or agreed to in writing, software
0014 // distributed under the License is distributed on an "AS IS" BASIS,
0015 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
0016 // See the License for the specific language governing permissions and
0017 // limitations under the License.
0018 */
0019 
0020 #include <ct_lvtqtc_alg_transitive_reduction.h>
0021 
0022 #include <ct_lvtqtc_edgecollection.h>
0023 #include <ct_lvtqtc_lakosentity.h>
0024 #include <ct_lvtqtc_lakosrelation.h>
0025 
0026 #include <memory>
0027 
0028 #include <QDebug>
0029 #include <QRunnable>
0030 
0031 namespace {
0032 constexpr bool debugMode = false;
0033 // start to spit information on the command line.
0034 
0035 QString S(const std::string& string)
0036 {
0037     return QString::fromStdString(string);
0038 }
0039 } // namespace
0040 
0041 using namespace Codethink::lvtqtc;
0042 
0043 class AlgorithmTransitiveReduction::RunnableThread : public QRunnable {
0044     // The ancient version of Qt in appimage (5.11) doesn't support creating a
0045     // QRunnable from a lambda so we have to write a lambda by hand here :(
0046   private:
0047     // PRIVATE DATA
0048     AlgorithmTransitiveReduction& d_parent;
0049     LakosEntity *d_entity;
0050 
0051   public:
0052     RunnableThread(AlgorithmTransitiveReduction& parent, LakosEntity *entity): d_parent(parent), d_entity(entity)
0053     {
0054         setAutoDelete(true);
0055     }
0056 
0057     ~RunnableThread() override = default;
0058 
0059     // MUTATORS
0060     void run() override
0061     {
0062         d_parent.startLookingForRedundantEdgesOnNode(d_entity);
0063     }
0064 };
0065 
0066 void AlgorithmTransitiveReduction::setVertices(std::vector<LakosEntity *> entities)
0067 {
0068     d_entities = std::move(entities);
0069 }
0070 
0071 bool AlgorithmTransitiveReduction::hasRun() const
0072 {
0073     return d_hasRun;
0074 }
0075 
0076 bool AlgorithmTransitiveReduction::hasError() const
0077 {
0078     return d_hasError;
0079 }
0080 
0081 QString AlgorithmTransitiveReduction::errorMessage() const
0082 {
0083     return d_errorMessage;
0084 }
0085 
0086 std::unordered_map<LakosEntity *, std::vector<std::shared_ptr<EdgeCollection>>>&
0087 AlgorithmTransitiveReduction::redundantEdgesByNode()
0088 {
0089     return d_redundantEdgesByNode;
0090 }
0091 
0092 long long AlgorithmTransitiveReduction::totalRunTime() const
0093 {
0094     return d_totalTime;
0095 }
0096 
0097 void AlgorithmTransitiveReduction::reset()
0098 {
0099     d_hasRun = false;
0100     d_hasError = false;
0101     d_redundantEdgesByNode.clear();
0102     d_redundantEdges.clear();
0103     d_redundantEdgesCache.clear();
0104 }
0105 
0106 void AlgorithmTransitiveReduction::run()
0107 {
0108     if (d_entities.empty()) {
0109         return;
0110     }
0111 
0112     reset();
0113 
0114     d_hasRun = true;
0115     d_totalTimer.start();
0116 
0117     auto cacheKey = std::uintptr_t{0};
0118     for (auto *e : d_entities) {
0119         cacheKey += reinterpret_cast<std::uintptr_t>(e->internalNode());
0120     }
0121     if (d_redundantEdgesCache.count(cacheKey) > 0) {
0122         for (auto const& entity : d_entities) {
0123             for (const std::shared_ptr<EdgeCollection>& maybeRedundant : entity->edgesCollection()) {
0124                 auto& cache = d_redundantEdgesCache[cacheKey];
0125                 auto edge =
0126                     std::tuple<lvtldr::LakosianNode *, lvtldr::LakosianNode *>{maybeRedundant->from()->internalNode(),
0127                                                                                maybeRedundant->to()->internalNode()};
0128                 auto it = std::find_if(cache.cbegin(), cache.cend(), [&edge](auto const& e) {
0129                     return edge == e;
0130                 });
0131                 if (it == cache.cend()) {
0132                     continue;
0133                 }
0134 
0135                 d_redundantEdgesByNode[entity].push_back(maybeRedundant);
0136                 d_redundantEdges.insert(maybeRedundant);
0137             }
0138         }
0139     } else {
0140         for (LakosEntity *entity : d_entities) {
0141             // if the current entity has zero edges, it's impossible
0142             // that it has redundant edges. If it has just one edge,
0143             // the edge can't be redundant as there's no alternative
0144             // way to go to the node.
0145             const auto eSize = entity->edgesCollection().size();
0146             if (eSize == 0 || eSize == 1) {
0147                 if (debugMode) {
0148                     qDebug() << "entity" << S(entity->name()) << "can't have redundant edges";
0149                 }
0150                 continue;
0151             }
0152 
0153             // if we have more than one edge, one of them could be a redundant
0154             // edge. so let's search.
0155             QRunnable *runnable = new RunnableThread(*this, entity);
0156             d_pool.start(runnable);
0157         }
0158         d_pool.waitForDone();
0159 
0160         // Save to cache
0161         for (auto const& e : d_redundantEdges) {
0162             d_redundantEdgesCache[cacheKey].push_back({e->from()->internalNode(), e->to()->internalNode()});
0163         }
0164     }
0165 
0166     for (const std::shared_ptr<EdgeCollection>& redundant : d_redundantEdges) {
0167         redundant->setRedundant(true);
0168     }
0169 
0170     d_totalTime = d_totalTimer.elapsed();
0171     if (debugMode) {
0172         qDebug() << "Transitive Reduction took" << d_totalTime;
0173     }
0174 }
0175 
0176 void AlgorithmTransitiveReduction::startLookingForRedundantEdgesOnNode(LakosEntity *entity)
0177 {
0178     // here we iterate over all edges, twice.
0179     // the outer loop is where we will have the target entity, the
0180     // value we are looking for, the inner loop is the start of the
0181     // lookup.
0182     // the edgesCollection is necessarily the edges that are going from
0183     // the entity to other entities.
0184     if (debugMode) {
0185         qDebug() << "Starting to look for reduntant edges on node" << S(entity->name());
0186     }
0187 
0188     for (const std::shared_ptr<EdgeCollection>& maybeRedundant : entity->edgesCollection()) {
0189         if (debugMode) {
0190             qDebug() << "Testing to see if the edge " << S(entity->name()) << S(maybeRedundant->to()->name())
0191                      << "is redundant";
0192         }
0193         for (const std::shared_ptr<EdgeCollection>& lookupStart : entity->edgesCollection()) {
0194             // ignore the lookupStart if it's the same edge as the `maybeRedundant`.
0195             if (maybeRedundant == lookupStart) {
0196                 continue;
0197             }
0198             // This edge is already on the redundant set, ignore.
0199             {
0200                 std::shared_lock<std::shared_mutex> guard(d_collectionSerialiser);
0201                 if (d_redundantEdges.find(lookupStart) != std::end(d_redundantEdges)) {
0202                     continue;
0203                 }
0204             }
0205 
0206             // We are at the start of the search of transitiveness for the edge
0207             // `maybeRedundant`. Setup the initial data here, and start to walk
0208             // the graph. If we find a cycle, abort.
0209             if (debugMode) {
0210                 qDebug() << "Reseting visited";
0211             }
0212 
0213             std::unordered_set<LakosEntity *> visited;
0214             LakosEntity *maybeReduntantNode = maybeRedundant->to();
0215             LakosEntity *currentToNode = lookupStart->to();
0216             visited.insert(lookupStart->from());
0217             const bool found = recursiveTransitiveSearch(maybeReduntantNode, currentToNode, visited);
0218             if (found) {
0219                 if (debugMode) {
0220                     qDebug() << "Found redundant edge between" << QString::fromStdString(entity->name()) << "and"
0221                              << QString::fromStdString(maybeReduntantNode->name());
0222                 }
0223 
0224                 // An edge collection has 0 - N edges between the same two
0225                 // nodes. It's usually 1 though.
0226                 {
0227                     std::unique_lock<std::shared_mutex> guard(d_collectionSerialiser);
0228                     d_redundantEdgesByNode[entity].push_back(maybeRedundant);
0229                     d_redundantEdges.insert(maybeRedundant);
0230                 }
0231             }
0232         }
0233     }
0234 }
0235 
0236 bool AlgorithmTransitiveReduction::recursiveTransitiveSearch(LakosEntity *target,
0237                                                              LakosEntity *current,
0238                                                              std::unordered_set<LakosEntity *>& visited)
0239 {
0240     if (target == current) {
0241         return true;
0242     }
0243 
0244     // if it's already visited, we are on a loop, and we need to break away from it.
0245     if (visited.find(current) != std::end(visited)) {
0246         return false;
0247     }
0248 
0249     if (debugMode) {
0250         qDebug() << "Adding" << S(current->name()) << "To visited";
0251     }
0252 
0253     visited.insert(current);
0254     for (const std::shared_ptr<EdgeCollection>& edge : current->edgesCollection()) {
0255         LakosEntity *nextNode = edge->to();
0256 
0257         // This edge is already on the redundant set, so just abort here.
0258         {
0259             std::shared_lock<std::shared_mutex> guard(d_collectionSerialiser);
0260             if (d_redundantEdges.find(edge) != std::end(d_redundantEdges)) {
0261                 continue;
0262             }
0263         }
0264 
0265         if (debugMode) {
0266             if (!nextNode) {
0267                 qDebug() << "Entity" << S(current->name()) << " lacks a `to` node on the edge collection";
0268             } else {
0269                 qDebug() << "The next node from" << S(current->name()) << "is" << S(nextNode->name());
0270             }
0271         }
0272 
0273         const bool found = recursiveTransitiveSearch(target, nextNode, visited);
0274         if (found) {
0275             return true;
0276         }
0277     }
0278 
0279     return false;
0280 }