Skip to content

packages/engine/scram/src/fault_tree_analysis.cc

Implementation of fault tree analysis.

Namespaces

Name
scram
scram::core

Source code

cpp
/*
 * Copyright (C) 2014-2018 Olzhas Rakhimov
 * Copyright (C) 2023 OpenPRA ORG Inc.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */


#include "fault_tree_analysis.h"

#include <algorithm>
#include <cmath>
#include <exception>
#include <functional>
#include <memory>
#include <limits>
#include <optional>
#include <utility>
#include <vector>

#include "bdd.h"
#include <boost/range/algorithm.hpp>

#include "event.h"
#include "logger.h"
#include "model.h"
#include "parameter.h"
#include "probability_analysis.h"
#include "product_filter.h"

namespace scram::core {

    namespace {

        product_filter::FilterOptions BuildFilterOptions(const Settings &settings,
                                                         bool adaptive_active,
                                                         double adaptive_target) {
            product_filter::FilterOptions options;
            options.limit_order = settings.limit_order();
            options.cut_off = settings.cut_off();
            options.adaptive = adaptive_active;
            options.adaptive_target = adaptive_target;
            options.approximation = settings.approximation();
            options.exact_quantification =
                settings.algorithm() == Algorithm::kBdd && settings.approximation() == Approximation::kNone;
            return options;
        }

    }// namespace

    void Print(const ProductContainer &) {
        // Intentionally left blank to prevent excessive console output during analyses.
    }

        ProductContainer::ProductContainer(const Zbdd &products, const Pdag &graph,
                                                                             const ProductSummary *summary,
                                                                             std::shared_ptr<const ProductList> filtered_products)
                : products_(products), graph_(graph), size_(0),
                    filtered_products_(std::move(filtered_products)) {
        if (summary) {
                        size_ = summary->product_count;
            distribution_ = summary->distribution;
            product_events_.reserve(summary->event_indices.size());
            for (int index: summary->event_indices) {
                const int first_index = Pdag::kVariableStartIndex;
                const int last_index_exclusive = first_index + static_cast<int>(graph_.basic_events().size());
                assert(index >= first_index &&
                       index < last_index_exclusive &&
                       "Product summary basic event index exceeds bounds.");
                product_events_.push_back(graph_.basic_events()[index]);
            }
            boost::sort(product_events_, [](const mef::BasicEvent *lhs, const mef::BasicEvent *rhs) {
                return lhs->id() < rhs->id();
            });
            product_events_.erase(std::unique(product_events_.begin(), product_events_.end()), product_events_.end());
            return;
        }

        Pdag::IndexMap<bool> filter(graph_.basic_events().size());
        const int kFirstIndex = Pdag::kVariableStartIndex;
        const int kLastIndexExclusive = kFirstIndex + static_cast<int>(graph_.basic_events().size());
        for (const std::vector<int> &product: products_) {
            int order_index = product.empty() ? 0 : product.size() - 1;
            if (distribution_.size() <= order_index)
                distribution_.resize(order_index + 1);
            distribution_[order_index]++;
            ++size_;

            for (int i: product) {
                i = std::abs(i);
                if (i < kFirstIndex || i >= kLastIndexExclusive)
                    continue;
                if (filter[i])
                    continue;
                filter[i] = true;
                product_events_.push_back(graph_.basic_events()[i]);
            }
        }
        boost::sort(product_events_, [](const mef::BasicEvent *lhs, const mef::BasicEvent *rhs) {
            return lhs->id() < rhs->id();
        });
        product_events_.erase(std::unique(product_events_.begin(), product_events_.end()), product_events_.end());
    }

    ProductContainer::Iterator::Iterator() = default;

    ProductContainer::Iterator::Iterator(const Pdag &graph, Zbdd::const_iterator it)
        : source_(Source::kZbdd), graph_(&graph) {
        zbdd_it_.emplace(it);
    }

    ProductContainer::Iterator::Iterator(const Pdag &graph,
                                         std::shared_ptr<const ProductList> filtered,
                                         ProductList::const_iterator it)
        : source_(Source::kFiltered), filtered_(std::move(filtered)),
          filtered_it_(it), graph_(&graph) {}

    void ProductContainer::Iterator::increment() {
        cache_.reset();
        if (source_ == Source::kZbdd) {
            assert(zbdd_it_ && "Increment on uninitialized ZBDD iterator.");
            ++(*zbdd_it_);
        } else {
            ++filtered_it_;
        }
    }

    bool ProductContainer::Iterator::equal(const Iterator &other) const {
        if (graph_ == nullptr || other.graph_ == nullptr)
            return graph_ == other.graph_;
        if (source_ != other.source_)
            return false;
        if (graph_ != other.graph_)
            return false;
        if (source_ == Source::kZbdd) {
            if (!zbdd_it_ || !other.zbdd_it_)
                return zbdd_it_.has_value() == other.zbdd_it_.has_value();
            return *zbdd_it_ == *other.zbdd_it_;
        }
        if (filtered_.get() != other.filtered_.get())
            return false;
        return filtered_it_ == other.filtered_it_;
    }

    const Product &ProductContainer::Iterator::dereference() const {
        assert(graph_ && "Dereferencing invalid iterator.");
        if (source_ == Source::kZbdd) {
            assert(zbdd_it_ && "Dereferencing uninitialized ZBDD iterator.");
            cache_.emplace(**zbdd_it_, *graph_);
        } else {
            cache_.emplace(*filtered_it_, *graph_);
        }
        return *cache_;
    }

    ProductContainer::Iterator ProductContainer::begin() const {
        if (filtered_products_)
            return Iterator(graph_, filtered_products_, filtered_products_->cbegin());
        return Iterator(graph_, products_.begin());
    }

    ProductContainer::Iterator ProductContainer::end() const {
        if (filtered_products_)
            return Iterator(graph_, filtered_products_, filtered_products_->cend());
        return Iterator(graph_, products_.end());
    }

    double Product::p() const {
        double p = 1;
        for (const Literal &literal: *this) {
            p *= literal.complement ? 1 - literal.event.p() : literal.event.p();
        }
        return p * graph_.initiating_event_frequency();
    }

    FaultTreeAnalysis::FaultTreeAnalysis(const mef::Gate &root,
                                         const Settings &settings,
                                         const mef::Model *model)
        : Analysis(settings), top_event_(root), model_(model) {}

    void FaultTreeAnalysis::Analyze()  {
        CLOCK(analysis_time);
        graph_ = std::make_unique<Pdag>(top_event_, Analysis::settings().ccf_analysis(), model_);
        graph_->initiating_event_frequency(initiating_event_frequency_);
        adaptive_mode_used_ = false;
        adaptive_target_probability_ = -1.0;
        last_summary_.reset();
        this->Preprocess(graph_.get());
#ifndef NDEBUG
        if (Analysis::settings().preprocessor)
            return;  // Preprocessor only option.
#endif
        // If products are required (most cases), run the algorithm to enumerate.
        // Otherwise (BDD probability-only kNone), skip product generation.
        if (Analysis::settings().requires_products()) {
            CLOCK(algo_time);
            LOG(DEBUG2) << "Launching the algorithm...";
            const Zbdd &products = this->GenerateProducts(graph_.get());
            const bool adaptive_requested = Analysis::settings().adaptive();
            double exact_probability = -1.0;
            bool adaptive_active = false;

            if (adaptive_requested) {
                exact_probability = GetExactProbabilityValue();
                adaptive_active = exact_probability > 0.0;
                if (!adaptive_active) {
                    LOG(WARNING) << "Adaptive quantification requested, but exact probability is unavailable."
                                 << " Falling back to cut-off filtering.";
                }
            }

            std::shared_ptr<ProductSummary::ProductList> filtered_products;
            const auto consumer = [&filtered_products](const std::vector<int> &product, double) {
                if (!filtered_products)
                    filtered_products = std::make_shared<ProductSummary::ProductList>();
                filtered_products->push_back(product);
            };

            const auto options = BuildFilterOptions(Analysis::settings(), adaptive_active, exact_probability);
            ProductSummary summary = product_filter::FilterProducts(products, *graph_, options, consumer);

            if (adaptive_active) {
                LOG(DEBUG2) << "Adaptive quantification kept " << summary.product_count
                            << " of " << summary.original_product_count << " products.";
                LOG(DEBUG2) << "Exact probability target: " << exact_probability;
                if (summary.cut_off_applied) {
                    LOG(DEBUG2) << "Adaptive convergence cut-off: " << summary.applied_cut_off;
                } else {
                    LOG(DEBUG2) << "Adaptive convergence retained all products (no truncation).";
                }
            } else if (summary.cut_off_applied) {
                LOG(DEBUG2) << "Cut-off retained " << summary.product_count
                            << " of " << summary.original_product_count << " products.";
            }
            LOG(DEBUG2) << "The algorithm finished in " << DUR(algo_time);
            LOG(DEBUG2) << "# of products: " << summary.product_count;
            if (!adaptive_active && summary.cut_off_applied) {
                LOG(DEBUG2) << "Cut-off " << Analysis::settings().cut_off() << " pruned "
                            << summary.pruned_products << " products.";
            } else if (!adaptive_active && summary.pruned_products > 0) {
                LOG(DEBUG2) << "Order limit " << Analysis::settings().limit_order() << " retained "
                            << summary.product_count << " of " << summary.original_product_count
                            << " products.";
            }
            adaptive_mode_used_ = adaptive_active;
            adaptive_target_probability_ = adaptive_active ? exact_probability : -1.0;
            last_summary_ = summary;
            Analysis::AddAnalysisTime(DUR(analysis_time));
            CLOCK(store_time);
            std::shared_ptr<const ProductSummary::ProductList> filtered_products_const;
            if (filtered_products) {
                filtered_products_const = std::static_pointer_cast<const ProductSummary::ProductList>(filtered_products);
            }
            Store(products, *graph_, summary, std::move(filtered_products_const));
            LOG(DEBUG2) << "Stored the result for reporting in " << DUR(store_time);
        } else {
            // For BDD probability-only mode, algorithm invocation is deferred to
            // the ProbabilityAnalyzer which constructs/evaluates directly on BDD.
            LOG(DEBUG2) << "Skipping product enumeration (BDD probability-only mode).";
            Analysis::AddAnalysisTime(DUR(analysis_time));
        }
    }

    double FaultTreeAnalysis::GetExactProbabilityValue() const {
        return ComputeAdaptiveTargetProbability();
    }

    double FaultTreeAnalysis::ComputeAdaptiveTargetProbability() const {
        if (!Analysis::settings().adaptive())
            return -1.0;

        LOG(DEBUG2) << "Adaptive quantification: deriving exact probability via internal BDD run.";

        Settings oracle_settings = Analysis::settings();
        oracle_settings.adaptive(false);
        oracle_settings.prime_implicants(false);
        oracle_settings.importance_analysis(false);
        oracle_settings.uncertainty_analysis(false);
        oracle_settings.safety_integrity_levels(false);
        oracle_settings.skip_products(false);
        oracle_settings.probability_analysis(true);
        oracle_settings.algorithm(Algorithm::kBdd);
        oracle_settings.approximation(Approximation::kNone);

        try {
            FaultTreeAnalyzer<Bdd> oracle_analyzer(top_event_, oracle_settings, model_);
            oracle_analyzer.initiating_event_frequency(initiating_event_frequency_);
            oracle_analyzer.Analyze();

            mef::MissionTime *mission_time_ptr = nullptr;
            std::optional<mef::MissionTime> fallback_time;
            if (model_) {
                mission_time_ptr = &const_cast<mef::Model *>(model_)->mission_time();
            } else {
                fallback_time.emplace(Analysis::settings().mission_time());
                mission_time_ptr = &fallback_time.value();
            }

            ProbabilityAnalyzer<Bdd> oracle_probability(&oracle_analyzer, mission_time_ptr);
            oracle_probability.Analyze();
            const double exact_probability = oracle_probability.p_total();
            LOG(DEBUG2) << "Adaptive quantification: BDD-derived probability "
                        << exact_probability;
            return exact_probability;
        } catch (const std::exception &ex) {
            LOG(WARNING) << "Adaptive quantification: failed to compute exact probability via BDD: " << ex.what();
        } catch (...) {
            LOG(WARNING) << "Adaptive quantification: failed to compute exact probability via BDD (unknown error).";
        }

        return -1.0;
    }

    void FaultTreeAnalysis::Store(const Zbdd &products,
                                  const Pdag &graph,
                                  const ProductSummary &summary,
                                  std::shared_ptr<const ProductSummary::ProductList> filtered_products)  {
        // Special cases of sets.
        if (products.empty()) {
            Analysis::AddWarning("The set is NULL/Empty.");
        } else if (products.base()) {
            Analysis::AddWarning("The set is UNITY/Base.");
        }
        if (summary.cut_off_applied && summary.product_count == 0) {
            Analysis::AddWarning("All products were removed by the cut-off threshold.");
        }
        filtered_products_ = std::move(filtered_products);
        products_ = std::make_unique<const ProductContainer>(products, graph, &summary, filtered_products_);

#ifndef NDEBUG
        for (const Product &product: *products_)
            assert(product.size() <= Analysis::settings().limit_order() &&
                   "Miscalculated product sets with larger-than-required order.");

        if (Analysis::settings().print)
            Print(*products_);
#endif
    }

}// namespace scram::core

Updated on 2026-01-09 at 21:59:13 +0000