Skip to content

packages/engine/scram/src/risk_analysis.cc

Implementation of risk analysis handler.

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 "risk_analysis.h"

#include <iostream>
#include "bdd.h"
#include "event.h"
#include "expression/random_deviate.h"
#include "ext/scope_guard.h"
#include "fault_tree.h"
#include "logger.h"
#include "mocus.h"
#include "pdag.h"
#include "zbdd.h"

#include <unordered_set>
#include <string>

namespace scram::core {

RiskAnalysis::RiskAnalysis(mef::Model* model, const Settings& settings)
    : Analysis(settings), model_(model) {}

void RiskAnalysis::set_runtime_metrics(RuntimeMetrics metrics) {
  runtime_metrics_ = std::move(metrics);
}

void RiskAnalysis::Analyze()  {
  assert(results_.empty() && "Rerunning the analysis.");
  // Set the seed for the pseudo-random number generator if given explicitly.
  // Otherwise it defaults to the implementation dependent value.
  if (Analysis::settings().seed() >= 0)
    mef::RandomDeviate::seed(Analysis::settings().seed());

  if (model_->alignments().empty()) {
    RunAnalysis();
  } else {
    for (const mef::Alignment& alignment : model_->alignments()) {
      for (const mef::Phase& phase : alignment.phases())
        RunAnalysis(Context{alignment, phase});
    }
  }
}

void RiskAnalysis::RunAnalysis(const std::optional<Context> &context) {
  std::vector<std::pair<mef::HouseEvent*, bool>> house_events;
  ext::scope_guard restorator(
      [&house_events, this, init_time = model_->mission_time().value()] {
        model_->mission_time().value(init_time);
        Analysis::settings().mission_time(init_time);

        for (const std::pair<mef::HouseEvent*, bool>& entry : house_events)
          entry.first->state(entry.second);
      });

  if (context) {
    double mission_time = context->phase.time_fraction() * model_->mission_time().value();
    model_->mission_time().value(mission_time);
    Analysis::settings().mission_time(mission_time);

    for (const mef::SetHouseEvent* instruction : context->phase.instructions()) {
      auto it = model_->table<mef::HouseEvent>().find(instruction->name());
      assert(it != model_->table<mef::HouseEvent>().end() && "Invalid instruction.");
      mef::HouseEvent& house_event = *it;
      if (house_event.state() != instruction->state()) {
        house_events.emplace_back(&house_event, house_event.state());
        house_event.state(instruction->state());
      }
    }
  }

  // todo:: combine all initiating events and linked event trees into a unified PDAG if using MonteCarlo DirectEval Probability
  // note:: multiple initiating events may point to the same event tree, which is totally fine, it should be a simple
  // link in the PDAG
  for (const mef::InitiatingEvent& initiating_event : model_->initiating_events()) {
    if (initiating_event.event_tree()) {
      const double initiating_frequency = initiating_event.frequency_value();
      LOG(INFO) << "Running event tree analysis: " << initiating_event.name();
      auto eta = std::make_unique<EventTreeAnalysis>(initiating_event, Analysis::settings(), model_->context());
      eta->Analyze();

      for (EventTreeAnalysis::Result& result : eta->sequences()) {
          const mef::Sequence& sequence = result.sequence;
          LOG(INFO) << "Running analysis for sequence: " << sequence.name();
          
          CLOCK(sequence_analysis_time);
          results_.push_back({{std::pair<const mef::InitiatingEvent&, const mef::Sequence&>{initiating_event, sequence}, context}});
          RunAnalysis(*result.gate, &results_.back(), initiating_frequency);

          if (result.is_expression_only) {
            results_.back().fault_tree_analysis = nullptr;
            results_.back().importance_analysis = nullptr;
          }
          if (Analysis::settings().probability_analysis()) {
            // p_total() already includes initiating_frequency since it was set in the PDAG
            result.p_sequence = results_.back().probability_analysis->p_total();
          }
          const double sequence_total_time = DUR(sequence_analysis_time);
          // Store the total preprocessing/analysis time for this sequence
          results_.back().preprocessing_seconds = sequence_total_time;
          
          LOG(INFO) << "Finished analysis for sequence: " << sequence.name() 
                    << " in " << sequence_total_time << " seconds";
      }
      event_tree_results_.push_back({initiating_event, context, std::move(eta)});
      LOG(INFO) << "Finished event tree analysis: " << initiating_event.name();
    }
  }
    // if (combined_mc_path) {
    //     // -------------------------------------------------------------
    //     // Avoid re-analyzing fault-tree top events that have already
    //     // been processed as part of the event-tree sequences above.
    //     // -------------------------------------------------------------
    //     return;
    // }

    std::unordered_set<const mef::Gate*> gate_results;
    for (const auto &result : results_) {
        const auto &element = result.id.target;
        if (const auto val = std::get_if<const mef::Gate *>(&element)) {
            gate_results.insert(*val);
        }
    }

  for (const mef::FaultTree& ft : model_->fault_trees()) {
    for (const mef::Gate* target : ft.top_events()) {
      if (!gate_results.contains(target)) {
          LOG(INFO) << "Running analysis for gate: " << target->id();
          CLOCK(gate_analysis_time);
          results_.push_back({{target, context}});
          RunAnalysis(*target, &results_.back());
          const double gate_total_time = DUR(gate_analysis_time);
          // Store the total preprocessing/analysis time for this gate
          results_.back().preprocessing_seconds = gate_total_time;
          LOG(INFO) << "Finished analysis for gate: " << target->id() 
                    << " in " << gate_total_time << " seconds";
      } else {
          LOG(INFO) << "Not re-running analysis for gate: " << target->id();
      }
    }
  }
}

void RiskAnalysis::RunAnalysis(const mef::Gate& target, Result* result, double initiating_frequency)  {
  switch (Analysis::settings().algorithm()) {
    case Algorithm::kBdd:
      return RunAnalysis<Bdd>(target, result, initiating_frequency);
    case Algorithm::kZbdd:
      return RunAnalysis<Zbdd>(target, result, initiating_frequency);
    case Algorithm::kMocus:
        return RunAnalysis<Mocus>(target, result, initiating_frequency);
  }
}

template <class Algorithm>
void RiskAnalysis::RunAnalysis(const mef::Gate& target,
                               Result* result, double initiating_frequency)  {
  LOG(INFO) << "[RiskAnalysis::RunAnalysis] Starting for gate: " << target.id();
  LOG(INFO) << "[RiskAnalysis::RunAnalysis] Initiating event frequency: " << initiating_frequency;
  auto fta = std::make_unique<FaultTreeAnalyzer<Algorithm>>(target, Analysis::settings(), model_);
  fta->initiating_event_frequency(initiating_frequency);
  LOG(INFO) << "[RiskAnalysis::RunAnalysis] Calling fta->Analyze()...";
  fta->Analyze();
  LOG(INFO) << "[RiskAnalysis::RunAnalysis] fta->Analyze() complete.";
  if (Analysis::settings().probability_analysis()) {
    LOG(INFO) << "[RiskAnalysis::RunAnalysis] Probability analysis enabled, approximation: " << static_cast<int>(Analysis::settings().approximation());
    switch (Analysis::settings().approximation()) {
      case Approximation::kNone:
        LOG(INFO) << "[RiskAnalysis::RunAnalysis] Running with Bdd approximation...";
        RunAnalysis<Algorithm, Bdd>(fta.get(), result);
        break;
      case Approximation::kRareEvent:
        LOG(INFO) << "[RiskAnalysis::RunAnalysis] Running with RareEvent approximation...";
        RunAnalysis<Algorithm, RareEventCalculator>(fta.get(), result);
        break;
      case Approximation::kMcub:
        LOG(INFO) << "[RiskAnalysis::RunAnalysis] Running with MCUB approximation...";
        RunAnalysis<Algorithm, McubCalculator>(fta.get(), result);
        break;
    }
  }
  LOG(INFO) << "[RiskAnalysis::RunAnalysis] Moving fta to result...";
  result->fault_tree_analysis = std::move(fta);
  LOG(INFO) << "[RiskAnalysis::RunAnalysis] Complete!";
}

template <class Algorithm, class Calculator>
void RiskAnalysis::RunAnalysis(FaultTreeAnalyzer<Algorithm>* fta,
                               Result* result)  {
  auto pa = std::make_unique<ProbabilityAnalyzer<Calculator>>(fta, &model_->mission_time());

  pa->Analyze();
  if (Analysis::settings().importance_analysis()) {
    auto ia = std::make_unique<ImportanceAnalyzer<Calculator>>(pa.get());
    ia->Analyze();
    result->importance_analysis = std::move(ia);
  }
  if (Analysis::settings().uncertainty_analysis()) {
    auto ua = std::make_unique<UncertaintyAnalyzer<Calculator>>(pa.get());
    ua->Analyze();
    result->uncertainty_analysis = std::move(ua);
  }
  result->probability_analysis = std::move(pa);
}

// ---------------------------------------------------------------------------

}  // namespace scram::core

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