scram-node
This package contains C++ addons for the SCRAM engine.
Installation
nx build engine-scramRun Commands
Checkout package.json, to see what kind of commands you can run. Here is an example:
nx run engine-scram:test
nx runSCRAM Engine: Complete Workflow Documentation
Overview
SCRAM is a C++ engine that performs probabilistic risk assessment calculations for event trees linked with fault trees. This document provides a comprehensive explanation of the entire workflow from input processing to final analysis.
Table of Contents
- Input Processing and Model Construction
- PDAG Construction
- PDAG Preprocessing (5 Phases)
- Event Tree Analysis with Linked Fault Trees
- Analysis Execution
- Complete Example Workflow
1. Input Processing and Model Construction
1.1 Input Validation and Parsing
SCRAM accepts input in OpenPSA MEF (Model Exchange Format) XML files. The processing begins with validation and parsing:
// Main entry point in scram.cc
void RunScram(const po::variables_map& vm) {
// 1. Parse command line arguments and construct settings
scram::core::Settings settings;
ConstructSettings(vm, &settings);
// 2. Initialize model from input files
std::unique_ptr<scram::mef::Model> model =
scram::mef::Initializer(input_files, settings, vm.count("allow-extern"))
.model();
// 3. Perform risk analysis
scram::core::RiskAnalysis analysis(model.get(), settings);
analysis.Analyze();
}Input Processing Sequence:
User Input (XML)
↓
MEF Schema Validation ✅
↓
XML Parsing → MEF Objects (Model, FaultTree, Gate, BasicEvent, etc.)
↓
MEF Object Validation ✅
↓
Formula Creation (MEF Objects → MEF Formulas)
↓
Model Setup and Validation ✅
↓
PDAG Construction (MEF Formulas → PDAG)1.2 MEF Object Creation
The Initializer class processes XML files and creates MEF objects:
void Initializer::ProcessInputFile(const xml::Document& document) {
xml::Element root = document.root();
// Create model container
if (!model_) {
model_ = ConstructElement<Model>(root);
}
// Process different XML elements
for (const xml::Element& node : root.children()) {
if (node.name() == "define-fault-tree") {
DefineFaultTree(node); // Creates fault tree objects
} else if (node.name() == "define-event-tree") {
DefineEventTree(node); // Creates event tree objects
} else if (node.name() == "define-CCF-group") {
Register<CcfGroup>(node, "", RoleSpecifier::kPublic);
}
// ... other element types
}
}Example Input XML:
<opsa-mef>
<define-fault-tree name="system-failure">
<define-gate name="top-event">
<and>
<event name="component-a"/>
<event name="component-b"/>
</and>
</define-gate>
<define-basic-event name="component-a">
<exponential>
<parameter name="failure-rate">1.0e-3</parameter>
</exponential>
</define-basic-event>
<define-basic-event name="component-b">
<exponential>
<parameter name="failure-rate">2.0e-3</parameter>
</exponential>
</define-basic-event>
</define-fault-tree>
</opsa-mef>Resulting MEF Objects:
Model* model = new Model();
FaultTree* fault_tree = new FaultTree("system-failure");
Gate* top_event = new Gate("top-event");
BasicEvent* component_a = new BasicEvent("component-a");
BasicEvent* component_b = new BasicEvent("component-b");1.3 Formula Creation
MEF objects are converted to Boolean formulas during the initialization phase:
template <>
void Initializer::Define(const xml::Element& gate_node, Gate* gate) {
auto formulas = GetNonAttributeElements(gate_node);
gate->formula(GetFormula(*formulas.begin(), gate->base_path()));
}Resulting MEF Formula:
// top_event->formula() becomes:
Formula(AND, {component_a, component_b})Key Point: MEF Formulas are essential intermediate representations that:
- Provide validation of Boolean logic structure
- Enable complex logic transformations (IFF, IMPLY, etc.)
- Support dynamic modifications (house events, event trees)
- Serve as the input for PDAG construction
Why MEF Formulas Cannot Be Skipped: The MEF Formula layer cannot be eliminated because:
- PDAG Construction Expects Formulas: The
Pdagconstructor specifically takesmef::Gateobjects and accesses theirformula()method - Complex Logic Transformation: Many MEF connectives need transformation into basic AND/OR logic before analysis
- Validation Layer: Formulas provide validation before analysis begins
- Memory Management: Formulas handle complex Boolean expressions with shared sub-expressions
- Dynamic Modifications: Event trees with house events require formula cloning and modification
## 2. PDAG Construction
### 2.1 PDAG Creation from MEF Formulas
The PDAG (Propositional Directed Acyclic Graph) is constructed from MEF Formulas, not directly from fault trees:
```cpp
void FaultTreeAnalysis::Analyze() noexcept {
// Create PDAG from the top event's formula
graph_ = std::make_unique<Pdag>(top_event_,
Analysis::settings().ccf_analysis(),
model_);
// Preprocess the PDAG
this->Preprocess(graph_.get());
// Generate products for analysis
const Zbdd& products = this->GenerateProducts(graph_.get());
}PDAG Constructor:
Pdag::Pdag(const mef::Gate& root, bool ccf, const mef::Model* model) noexcept {
ProcessedNodes nodes;
// 1. Gather all variables (basic events) from the gate's formula
GatherVariables(root.formula(), ccf, &nodes);
// 2. Construct gates recursively from the formula structure
root_ = ConstructGate(root.formula(), ccf, &nodes);
// 3. Process substitutions if any
if (model) {
ProcessSubstitutions(model, &nodes);
}
}2.2 PDAG Data Structures and Node Connections
The PDAG uses a sophisticated bidirectional connection system with multiple data structures to efficiently manage relationships between nodes:
Core Data Structures
NodeParentManager Class:
class NodeParentManager {
using Parent = std::pair<int, GateWeakPtr>; // Parent index and weak pointer
using ParentMap = ext::linear_map<int, GateWeakPtr, ext::MoveEraser>;
private:
ParentMap parents_; // All registered parents of this node
};Gate Class Data Members:
class Gate : public Node {
private:
ArgSet args_; // Set of argument indices (positive/negative)
ArgMap<Gate> gate_args_; // Map of gate arguments: index → shared_ptr<Gate>
ArgMap<Variable> variable_args_; // Map of variable arguments: index → shared_ptr<Variable>
ConstantPtr constant_; // Single constant argument (if any)
};Bidirectional Connection System
Each node maintains two types of connections:
Child-to-Parent Connections (Upward):
- Every node (Gate, Variable, Constant) inherits from
NodeParentManager - Stores weak pointers to its parent gates in
parents_map - Key: parent gate's index, Value: weak pointer to parent gate
- Uses
ext::linear_mapfor efficient small-scale lookups
Parent-to-Child Connections (Downward):
- Gates store shared pointers to their child arguments
- Three separate containers for different argument types:
gate_args_: Child gatesvariable_args_: Child variables (basic events)constant_: Single constant child
args_set stores all argument indices (positive = normal, negative = complement)
Connection Management Methods
Adding Arguments (Parent → Child):
template <class T>
void Gate::AddArg(int index, const std::shared_ptr<T>& arg) noexcept {
args_.insert(index); // Add to index set
mutable_args<T>().data().emplace_back(index, arg); // Add to type-specific map
arg->AddParent(shared_from_this()); // Establish child → parent link
}Adding Parents (Child → Parent):
void NodeParentManager::AddParent(const GatePtr& gate) {
parents_.data().emplace_back(gate->index(), gate); // Weak pointer
}Data Structure Design Choices
Why ext::linear_map instead of std::unordered_map?
- Cache-friendly: Contiguous memory layout for small collections
- Performance: Outperforms hash maps for small numbers of entries (up to 50 entries)
- Memory efficiency: No hash table overhead
- Predictable iteration: Preserves insertion order
Why Weak Pointers for Parents?
- Prevents circular references: Avoids memory leaks
- Automatic cleanup: When parent is destroyed, weak pointer becomes invalid
- Safe traversal: Can check if parent still exists before accessing
Why Shared Pointers for Children?
- Ownership semantics: Gate owns its children
- Automatic cleanup: Children are destroyed when last parent is destroyed
- Reference counting: Multiple parents can share the same child
Example Connection Structure
Gate G1 (index: 10)
├── parents_: {} (root gate)
├── args_: {2, 3, 15} (indices of children)
├── gate_args_: {15 → shared_ptr<Gate G2>}
└── variable_args_: {2 → shared_ptr<Variable A>, 3 → shared_ptr<Variable B>}
Gate G2 (index: 15)
├── parents_: {10 → weak_ptr<Gate G1>}
├── args_: {4, 5}
└── variable_args_: {4 → shared_ptr<Variable C>, 5 → shared_ptr<Variable D>}
Variable A (index: 2)
├── parents_: {10 → weak_ptr<Gate G1>}
└── (no children)
Variable B (index: 3)
├── parents_: {10 → weak_ptr<Gate G1>}
└── (no children)Traversal and Access Patterns
Finding Children of a Gate:
// Get all gate children
for (const auto& arg : gate.args<Gate>()) {
GatePtr child_gate = arg.second; // shared_ptr<Gate>
// Process child_gate
}
// Get all variable children
for (const auto& arg : gate.args<Variable>()) {
VariablePtr child_var = arg.second; // shared_ptr<Variable>
// Process child_var
}Finding Parents of a Node:
// Get all parents
for (const auto& parent : node.parents()) {
if (auto parent_gate = parent.second.lock()) { // Convert weak_ptr to shared_ptr
// Process parent_gate
}
}Performance Characteristics
- Lookup time: O(N) for linear_map (but very fast for small N due to cache locality)
- Memory usage: Minimal overhead compared to hash maps
- Traversal: Efficient bidirectional traversal
- Modification: Fast insertion/deletion with MoveEraser policy
- Cache performance: Contiguous memory layout provides excellent cache locality
- Memory safety: Automatic cleanup through shared_ptr/weak_ptr reference counting
2.3 Variable and Gate Indexing
SCRAM uses the following indexing system:
void Pdag::GatherVariables(const mef::BasicEvent& basic_event, bool ccf,
ProcessedNodes* nodes) noexcept {
if (ccf && basic_event.HasCcf()) {
// Handle CCF groups
if (nodes->gates.emplace(&basic_event.ccf_gate(), nullptr).second)
GatherVariables(basic_event.ccf_gate().formula(), ccf, nodes);
} else {
VariablePtr& var = nodes->variables[&basic_event];
if (!var) {
basic_events_.push_back(&basic_event);
var = std::make_shared<Variable>(this);
// Variables get indices starting from kVariableStartIndex (2)
assert((kVariableStartIndex + basic_events_.size() - 1) == var->index());
}
}
}Indexing Scheme:
- Index 1: Constant TRUE
- Index -1: Constant FALSE
- Indices 2, 3, 4, ...: Variables (basic events)
- Higher indices: Gates
2.4 Gate Construction
PDAG gates are constructed recursively from MEF formulas:
GatePtr Pdag::ConstructGate(const mef::Formula& formula, bool ccf,
ProcessedNodes* nodes) noexcept {
Connective type = static_cast<Connective>(formula.connective());
auto parent = std::make_shared<Gate>(type, this);
// Handle special cases
switch (type) {
case kNot:
case kNand:
case kNor:
case kXor:
coherent_ = false;
break;
case kAtleast:
parent->min_number(*formula.min_number());
break;
}
// Add arguments from the formula
for (const mef::Formula::Arg& arg : formula.args()) {
if (arg.complement)
coherent_ = false;
AddArg(parent, arg.event, arg.complement, ccf, nodes);
}
return parent;
}3. PDAG Preprocessing (5 Phases)
The PDAG undergoes 5 phases of preprocessing to optimize it for analysis:
Phase 1: Basic Cleanup and Partial Normalization
void Preprocessor::RunPhaseOne() noexcept {
// Remove NULL gates
if (graph_->HasNullGates()) {
graph_->RemoveNullGates();
if (graph_->IsTrivial()) return;
}
// Partial normalization for non-coherent graphs
if (!graph_->coherent()) {
NormalizeGates(/*full=*/false);
}
}What happens:
- Removes pass-through gates that don't contribute to logic
- Converts simple non-coherent gates (NOT, NAND, NOR) to AND/OR form
Example:
Before: NOR(A,B)
After: OR(A,B) with negated argumentsPhase 2: Structural Optimization
void Preprocessor::RunPhaseTwo() noexcept {
pdag::Transform(graph_,
[this](Pdag*) { while (ProcessMultipleDefinitions()) continue; },
[this](Pdag*) { DetectModules(); },
[this](Pdag*) { while (CoalesceGates(/*common=*/false)) continue; },
[this](Pdag*) { MergeCommonArgs(); },
[this](Pdag*) { DetectDistributivity(); },
[this](Pdag*) { DetectModules(); },
[this](Pdag*) { BooleanOptimization(); },
[this](Pdag*) { DecomposeCommonNodes(); },
[this](Pdag*) { DetectModules(); },
[this](Pdag*) { while (CoalesceGates(/*common=*/false)) continue; },
[this](Pdag*) { DetectModules(); });
}Key Transformations:
Multiple Definition Processing
void DetectMultipleDefinitions(const GatePtr& gate,
std::unordered_map<GatePtr, std::vector<GateWeakPtr>>* multi_def,
GateSet* unique_gates) noexcept {
if (gate->mark()) return;
gate->mark(true);
if (!gate->module()) {
std::pair<GatePtr, bool> ret = unique_gates->insert(gate);
if (!ret.second) { // The gate is duplicate
(*multi_def)[ret.first].push_back(gate);
return;
}
}
for (const Gate::Arg<Gate>& arg : gate->args<Gate>()) {
DetectMultipleDefinitions(arg.second, multi_def, unique_gates);
}
}Example:
Before: G1 = AND(A,B), G2 = AND(A,B)
After: G1 = AND(A,B), G2 = G1 (reference)Module Detection
void Preprocessor::DetectModules() noexcept {
// 1. Assign timing to all nodes using DFS
graph_->Clear<Pdag::kVisit>();
AssignTiming(0, root_gate);
// 2. Find modules
graph_->Clear<Pdag::kGateMark>();
FindModules(root_gate);
}
void Preprocessor::FindModules(const GatePtr& gate) noexcept {
if (gate->mark()) return;
gate->mark(true);
int enter_time = gate->EnterTime();
int exit_time = gate->ExitTime();
int min_time = enter_time;
int max_time = exit_time;
// Check if all arguments are within gate's time range
for (const Gate::Arg<Gate>& arg : gate->args<Gate>()) {
const GatePtr& arg_gate = arg.second;
FindModules(arg_gate);
if (IsSubgraphWithinGraph(arg_gate, enter_time, exit_time)) {
// Argument is within this gate's scope
} else {
// Argument extends beyond this gate's scope
min_time = std::min(min_time, arg_gate->min_time());
max_time = std::max(max_time, arg_gate->max_time());
}
}
// Determine if this gate is a module
if (!gate->module() && min_time == enter_time && max_time == exit_time) {
gate->module(true);
}
}Example:
G1 = AND(A,B) // Module: all children within G1's time range
G2 = OR(G1,C) // Not a module: G1 is outside G2's time rangeGate Coalescing
Gate coalescing transforms nested gates of the same type into a flattened structure by moving child arguments up and removing intermediate gates.
Coalescing Algorithm:
bool Preprocessor::CoalesceGates(const GatePtr& gate, bool common) noexcept {
if (gate->mark()) return false;
gate->mark(true);
Connective target_type = kNull;
switch (gate->type()) {
case kNand:
case kAnd:
target_type = kAnd;
break;
case kNor:
case kOr:
target_type = kOr;
break;
}
std::vector<GatePtr> to_join;
for (const Gate::Arg<Gate>& arg : gate->args<Gate>()) {
const GatePtr& arg_gate = arg.second;
if (arg_gate->type() == target_type) {
to_join.push_back(arg_gate);
}
}
// Join gates of the same type
for (const GatePtr& arg : to_join) {
gate->CoalesceGate(arg);
}
return !to_join.empty();
}Core Coalescing Implementation:
void Gate::CoalesceGate(const GatePtr& arg_gate) noexcept {
// 1. Move all gate arguments from arg_gate to this gate
for (const auto& arg : arg_gate->gate_args_) {
AddArg(arg); // This calls AddArg(index, shared_ptr)
if (constant()) return;
}
// 2. Move all variable arguments from arg_gate to this gate
for (const auto& arg : arg_gate->variable_args_) {
AddArg(arg); // This calls AddArg(index, shared_ptr)
if (constant()) return;
}
// 3. Remove the arg_gate from this gate's arguments
args_.erase(arg_gate->index());
gate_args_.erase(arg_gate->index());
// 4. Remove this gate as parent of arg_gate
arg_gate->EraseParent(Node::index());
}Example Transformation: AND(AND(A,B), AND(C,D)) → AND(A,B,C,D)
Before Coalescing:
Gate G1 (index: 10) - Root AND gate
├── parents_: {} (root gate)
├── args_: {15, 20} (indices of child gates)
├── gate_args_: {15 → shared_ptr<Gate G2>, 20 → shared_ptr<Gate G3>}
└── variable_args_: {}
Gate G2 (index: 15) - First inner AND gate
├── parents_: {10 → weak_ptr<Gate G1>}
├── args_: {2, 3} (indices of variables A, B)
├── gate_args_: {}
└── variable_args_: {2 → shared_ptr<Variable A>, 3 → shared_ptr<Variable B>}
Gate G3 (index: 20) - Second inner AND gate
├── parents_: {10 → weak_ptr<Gate G1>}
├── args_: {4, 5} (indices of variables C, D)
├── gate_args_: {}
└── variable_args_: {4 → shared_ptr<Variable C>, 5 → shared_ptr<Variable D>}
Variable A (index: 2)
├── parents_: {15 → weak_ptr<Gate G2>}
└── (no children)
Variable B (index: 3)
├── parents_: {15 → weak_ptr<Gate G2>}
└── (no children)
Variable C (index: 4)
├── parents_: {20 → weak_ptr<Gate G3>}
└── (no children)
Variable D (index: 5)
├── parents_: {20 → weak_ptr<Gate G3>}
└── (no children)After Coalescing:
Gate G1 (index: 10) - Root AND gate (now directly connected to variables)
├── parents_: {} (root gate)
├── args_: {2, 3, 4, 5} (indices of variables A, B, C, D)
├── gate_args_: {} (no more intermediate gates)
└── variable_args_: {2 → shared_ptr<Variable A>, 3 → shared_ptr<Variable B>,
4 → shared_ptr<Variable C>, 5 → shared_ptr<Variable D>}
Gate G2 (index: 15) - Orphaned (no parents, will be garbage collected)
├── parents_: {} (empty - no longer referenced)
├── args_: {2, 3} (still has children, but no parents)
├── gate_args_: {}
└── variable_args_: {2 → shared_ptr<Variable A>, 3 → shared_ptr<Variable B>}
Gate G3 (index: 20) - Orphaned (no parents, will be garbage collected)
├── parents_: {} (empty - no longer referenced)
├── args_: {4, 5} (still has children, but no parents)
├── gate_args_: {}
└── variable_args_: {4 → shared_ptr<Variable C>, 5 → shared_ptr<Variable D>}
Variable A (index: 2)
├── parents_: {10 → weak_ptr<Gate G1>, 15 → weak_ptr<Gate G2>}
└── (no children)
Variable B (index: 3)
├── parents_: {10 → weak_ptr<Gate G1>, 15 → weak_ptr<Gate G2>}
└── (no children)
Variable C (index: 4)
├── parents_: {10 → weak_ptr<Gate G1>, 20 → weak_ptr<Gate G3>}
└── (no children)
Variable D (index: 5)
├── parents_: {10 → weak_ptr<Gate G1>, 20 → weak_ptr<Gate G3>}
└── (no children)Key Pointer Changes:
Child-to-Parent Links (Variables → Gates):
- Variables A, B, C, D gain a new parent (G1)
- Variables A, B keep their old parent (G2) - but G2 becomes orphaned
- Variables C, D keep their old parent (G3) - but G3 becomes orphaned
Parent-to-Child Links (Gates → Variables):
- G1 gains direct connections to A, B, C, D
- G1 loses connections to G2 and G3
- G2 and G3 keep their connections to their variables but become unreachable
Intermediate Gates (G2, G3):
- Lose their parent (G1)
- Keep their children (A, B, C, D) but become orphaned
- Will be garbage collected when their shared_ptr reference count reaches 0
Memory Management:
- G2 and G3 become orphaned nodes with no incoming references
- They will be automatically cleaned up by the shared_ptr reference counting
- The variables (A, B, C, D) are now shared between the root gate and orphaned gates
- When orphaned gates are destroyed, variables will lose those parent references
Final State After Garbage Collection:
Gate G1 (index: 10) - Root AND gate
├── parents_: {}
├── args_: {2, 3, 4, 5}
├── gate_args_: {}
└── variable_args_: {2 → shared_ptr<Variable A>, 3 → shared_ptr<Variable B>,
4 → shared_ptr<Variable C>, 5 → shared_ptr<Variable D>}
Variable A (index: 2)
├── parents_: {10 → weak_ptr<Gate G1>} // Only G1 remains
└── (no children)
Variable B (index: 3)
├── parents_: {10 → weak_ptr<Gate G1>} // Only G1 remains
└── (no children)
Variable C (index: 4)
├── parents_: {10 → weak_ptr<Gate G1>} // Only G1 remains
└── (no children)
Variable D (index: 5)
├── parents_: {10 → weak_ptr<Gate G1>} // Only G1 remains
└── (no children)
// G2 and G3 are destroyed and no longer existThis transformation flattens the hierarchy by moving all leaf nodes (variables) directly under the root gate, eliminating the intermediate gates while preserving the logical structure. The memory management is handled automatically through shared_ptr reference counting.
Boolean Optimization
void Preprocessor::BooleanOptimization() noexcept {
// Find nodes with multiple parents
std::vector<GateWeakPtr> common_gates;
std::vector<std::weak_ptr<Variable>> common_variables;
GatherCommonNodes(&common_gates, &common_variables);
// Process each common node for optimization
for (const auto& gate : common_gates) {
ProcessCommonNode(gate);
}
}
template <class N>
void Preprocessor::ProcessCommonNode(const std::weak_ptr<N>& common_node) noexcept {
// 1. Mark ancestors
GatePtr root;
MarkAncestors(common_node, &root);
// 2. Simulate failure propagation
int mult_tot = PropagateState(root, common_node);
// 3. Identify redundant connections
std::unordered_map<int, GateWeakPtr> destinations;
CollectStateDestinations(root, common_node->index(), &destinations);
// 4. Remove redundant paths
if (destinations.size() > 0 && destinations.size() < mult_tot) {
std::vector<GateWeakPtr> redundant_parents;
CollectRedundantParents(common_node, &destinations, &redundant_parents);
ProcessRedundantParents(common_node, redundant_parents);
}
}Example:
Before: G1→A, G2→A, G3→A (all paths lead to same result)
After: G1→A, G2→A (G3 removed as redundant)Phase 3: Full Normalization
void Preprocessor::RunPhaseThree() noexcept {
assert(!graph_->normal());
NormalizeGates(/*full=*/true);
graph_->normal(true);
if (graph_->IsTrivial()) return;
RunPhaseTwo(); // Apply optimizations to normalized graph
}Gate Normalization Algorithms:
XOR Gate Normalization
void Preprocessor::NormalizeXorGate(const GatePtr& gate) noexcept {
assert(gate->args().size() == 2);
// XOR(A,B) → OR(AND(A,NOT(B)), AND(NOT(A),B))
auto gate_one = std::make_shared<Gate>(kAnd, graph_);
auto gate_two = std::make_shared<Gate>(kAnd, graph_);
gate->type(kOr);
// First argument: A
auto it = gate->args().begin();
gate->ShareArg(*it, gate_one);
gate->ShareArg(*it, gate_two);
gate_two->NegateArg(*it);
// Second argument: B
++it;
gate->ShareArg(*it, gate_one);
gate_one->NegateArg(*it);
gate->ShareArg(*it, gate_two);
gate->EraseArgs();
gate->AddArg(gate_one);
gate->AddArg(gate_two);
}K/N Gate Normalization
void Preprocessor::NormalizeAtleastGate(const GatePtr& gate) noexcept {
assert(gate->type() == kAtleast);
int min_number = gate->min_number();
// Base cases
if (gate->args().size() == min_number) {
gate->type(kAnd); // All must fail
return;
}
if (min_number == 1) {
gate->type(kOr); // At least one must fail
return;
}
// Recursive decomposition: K/N(A,B,C) → OR(AND(A, K-1/N-1(B,C)), K/N-1(B,C))
auto it = boost::max_element(gate->args(), [&gate](int lhs, int rhs) {
return gate->GetArg(lhs)->order() < gate->GetArg(rhs)->order();
});
auto first_arg = std::make_shared<Gate>(kAnd, graph_);
gate->TransferArg(*it, first_arg);
auto grand_arg = std::make_shared<Gate>(kAtleast, graph_);
first_arg->AddArg(grand_arg);
grand_arg->min_number(min_number - 1);
auto second_arg = std::make_shared<Gate>(kAtleast, graph_);
second_arg->min_number(min_number);
for (int index : gate->args()) {
gate->ShareArg(index, grand_arg);
gate->ShareArg(index, second_arg);
}
gate->type(kOr);
gate->EraseArgs();
gate->AddArg(first_arg);
gate->AddArg(second_arg);
// Recursively normalize sub-gates
NormalizeAtleastGate(grand_arg);
NormalizeAtleastGate(second_arg);
}Phase 4: Complement Propagation
void Preprocessor::RunPhaseFour() noexcept {
assert(!graph_->coherent());
// Handle root complement
if (graph_->complement()) {
const GatePtr& root = graph_->root();
if (root->type() == kOr || root->type() == kAnd) {
root->type(root->type() == kOr ? kAnd : kOr);
}
root->NegateArgs();
graph_->complement() = false;
}
// Propagate complements down to variables
std::unordered_map<int, GatePtr> complements;
graph_->Clear<Pdag::kGateMark>();
PropagateComplements(graph_->root(), false, &complements);
if (graph_->IsTrivial()) return;
RunPhaseTwo();
}Complement Propagation Algorithm:
void Preprocessor::PropagateComplements(const GatePtr& gate, bool keep_modules,
std::unordered_map<int, GatePtr>* complements) noexcept {
if (gate->mark()) return;
gate->mark(true);
std::vector<std::pair<int, GatePtr>> to_swap;
for (const Gate::Arg<Gate>& arg : gate->args<Gate>()) {
const GatePtr& arg_gate = arg.second;
if ((arg.first > 0) || (keep_modules && arg_gate->module())) {
PropagateComplements(arg_gate, keep_modules, complements);
continue;
}
// Apply De Morgan's laws
Connective type = arg_gate->type();
Connective complement_type = type == kOr ? kAnd : kOr;
GatePtr complement;
if (arg_gate->parents().size() == 1) {
// Reuse existing gate
arg_gate->type(complement_type);
arg_gate->NegateArgs();
complement = arg_gate;
} else {
// Create new complement gate
complement = arg_gate->Clone();
complement->type(complement_type);
complement->NegateArgs();
complements->emplace(arg_gate->index(), complement);
}
to_swap.emplace_back(arg.first, complement);
PropagateComplements(complement, keep_modules, complements);
}
// Apply the swaps
for (const auto& arg : to_swap) {
gate->EraseArg(arg.first);
gate->AddArg(arg.second);
}
}Example:
Before: AND(NOT(OR(A,B)), NOT(AND(C,D)))
After: AND(AND(NOT(A), NOT(B)), OR(NOT(C), NOT(D)))Phase 5: Final Optimization
void Preprocessor::RunPhaseFive() noexcept {
// Final gate coalescing (including common gates)
while (CoalesceGates(/*common=*/true)) continue;
if (graph_->IsTrivial()) return;
RunPhaseTwo(); // Apply all optimizations one final time
if (graph_->IsTrivial()) return;
while (CoalesceGates(/*common=*/true)) continue;
}4. Event Tree Analysis with Linked Fault Trees
4.1 Event Tree Processing
When analyzing event trees with linked fault trees, SCRAM creates a hierarchical PDAG structure:
void EventTreeAnalysis::Analyze() noexcept {
// Walk event tree paths and collect sequences
SequenceCollector collector{initiating_event_, *context_};
CollectSequences(initiating_event_.event_tree()->initial_state(), &collector);
// For each sequence, create a gate representing the sequence
for (auto& sequence : collector.sequences) {
auto gate = std::make_unique<mef::Gate>("__" + sequence.first->name());
// Process collected formulas and expressions
for (PathCollector& path_collector : sequence.second) {
if (path_collector.formulas.size() == 1) {
gate_formulas.push_back(std::move(path_collector.formulas.front()));
} else if (path_collector.formulas.size() > 1) {
// Combine multiple formulas with AND
mef::Formula::ArgSet arg_set;
for (mef::FormulaPtr& arg_formula : path_collector.formulas) {
arg_set.Add(make_gate(std::move(arg_formula)));
}
gate_formulas.push_back(
std::make_unique<mef::Formula>(mef::kAnd, std::move(arg_set)));
}
}
// Create final sequence gate
if (gate_formulas.size() == 1) {
gate->formula(std::move(gate_formulas.front()));
} else if (gate_formulas.size() > 1) {
// Combine multiple formulas with OR
mef::Formula::ArgSet arg_set;
for (mef::FormulaPtr& arg_formula : gate_formulas) {
arg_set.Add(make_gate(std::move(arg_formula)));
}
gate->formula(std::make_unique<mef::Formula>(mef::kOr, std::move(arg_set)));
}
sequences_.push_back({*sequence.first, std::move(gate), is_expression_only});
}
}4.2 House Event Integration
House events are applied to fault trees during sequence collection:
void Visit(const mef::SetHouseEvent* house_event) override {
collector_.path_collector_.set_instructions[house_event->name()] =
house_event->state();
}
void Visit(const mef::CollectFormula* collect_formula) override {
collector_.path_collector_.formulas.push_back(
core::Clone(collect_formula->formula(),
collector_.path_collector_.set_instructions,
collector_.clones_));
}House Event Processing:
std::unique_ptr<mef::Formula> Clone(const mef::Formula& formula,
const std::unordered_map<std::string, bool>& set_instructions,
std::vector<std::unique_ptr<mef::Event>>* clones) noexcept {
struct {
mef::Formula::ArgEvent operator()(mef::HouseEvent* arg) {
if (auto it = ext::find(set_house, arg->id())) {
if (it->second == arg->state()) {
return arg;
}
// Create clone with different state
auto clone = std::make_unique<mef::HouseEvent>(
arg->name(), "__clone__." + arg->id(), mef::RoleSpecifier::kPrivate);
clone->state(it->second);
auto* ptr = clone.get();
event_register->emplace_back(std::move(clone));
return ptr;
}
return arg;
}
// ... other operators
} cloner{set_instructions, clones};
// Apply cloner to all arguments
mef::Formula::ArgSet arg_set;
for (const mef::Formula::Arg& arg : formula.args()) {
arg_set.Add(std::visit(cloner, arg.event), arg.complement);
}
return std::make_unique<mef::Formula>(formula.connective(), std::move(arg_set),
formula.min_number(), formula.max_number());
}4.3 Integrated PDAG Structure
The final integrated PDAG combines event tree and fault tree levels:
Integrated PDAG:
TOP (Event Tree + Fault Trees)
├── Sequence 1 Gate
│ ├── House Event A (TRUE)
│ ├── Fault Tree 1 Gate (expanded)
│ │ ├── AND(A1, A2)
│ │ └── OR(A3, A4)
│ └── Fault Tree 2 Gate (expanded)
│ ├── Basic Event B1
│ └── Basic Event B2
├── Sequence 2 Gate
│ ├── House Event A (FALSE)
│ ├── Fault Tree 1 Gate (expanded with different house event settings)
│ └── Expression (Time calculation)
└── Sequence 3 Gate
├── House Event B (TRUE)
└── Fault Tree 3 Gate (expanded)
├── Basic Event C1
└── Basic Event C24.4 House Event Optimization
When house events are set to FALSE, SCRAM optimizes away those sequences:
template <>
void Gate::AddConstantArg<false>() noexcept {
switch (type_) {
case kAnd:
MakeConstant(false); // AND(FALSE, anything) = FALSE
break;
case kOr:
ReduceLogic(kNull); // OR(FALSE, B) = B
break;
// ... other cases
}
}
bool Pdag::IsTrivial() noexcept {
if (root_->constant()) {
// If root becomes a constant, the entire sequence is trivial
return true;
}
return false;
}Example:
Original: AND(A, B, C)
House Event: A = FALSE
Result: AND(FALSE, B, C) → FALSE (constant)
Analysis: Sequence probability = 0, eliminated5. Analysis Execution
5.1 Algorithm Selection
SCRAM supports multiple analysis algorithms:
void RiskAnalysis::RunAnalysis(const mef::Gate& target, Result* result) noexcept {
switch (Analysis::settings().algorithm()) {
case Algorithm::kBdd:
return RunAnalysis<Bdd>(target, result);
case Algorithm::kZbdd:
return RunAnalysis<Zbdd>(target, result);
case Algorithm::kMocus:
return RunAnalysis<Mocus>(target, result);
}
}5.2 BDD Analysis
template <class Algorithm>
void RiskAnalysis::RunAnalysis(const mef::Gate& target, Result* result) noexcept {
// Create fault tree analyzer
auto fta = std::make_unique<FaultTreeAnalyzer<Algorithm>>(
target, Analysis::settings(), model_);
fta->Analyze();
// Perform probability analysis
if (Analysis::settings().probability_analysis()) {
switch (Analysis::settings().approximation()) {
case Approximation::kNone:
RunAnalysis<Algorithm, Bdd>(fta.get(), result);
break;
case Approximation::kRareEvent:
RunAnalysis<Algorithm, RareEventCalculator>(fta.get(), result);
break;
case Approximation::kMcub:
RunAnalysis<Algorithm, McubCalculator>(fta.get(), result);
}
}
result->fault_tree_analysis = std::move(fta);
}5.3 Probability Calculation
double ProbabilityAnalyzer<Bdd>::CalculateProbability(
const Bdd::VertexPtr& vertex, bool mark,
const Pdag::IndexMap<double>& p_vars) noexcept {
if (vertex->terminal()) {
return 1;
}
Ite& ite = Ite::Ref(vertex);
if (ite.mark() == mark) {
return ite.p();
}
ite.mark(mark);
double p_var = 0;
if (ite.module()) {
const Bdd::Function& res = bdd_graph_->modules().find(ite.index())->second;
p_var = CalculateProbability(res.vertex, mark, p_vars);
if (res.complement) {
p_var = 1 - p_var;
}
} else {
p_var = p_vars[ite.index()];
}
double high = CalculateProbability(ite.high(), mark, p_vars);
double low = CalculateProbability(ite.low(), mark, p_vars);
if (ite.complement_edge()) {
low = 1 - low;
}
ite.p(p_var * high + (1 - p_var) * low);
return ite.p();
}6. Complete Example Workflow
6.1 Input Model
Fault Tree:
TOP = AND(OR(A,B), OR(C,D))
A: Basic Event (failure rate = 1e-3)
B: Basic Event (failure rate = 2e-3)
C: Basic Event (failure rate = 3e-3)
D: Basic Event (failure rate = 4e-3)Event Tree:
Initiating Event: System Failure
├── Sequence 1: ECCS Available = TRUE
│ ├── Fault Tree: ECCS System Failure
│ └── Fault Tree: Containment Failure
└── Sequence 2: ECCS Available = FALSE
└── Fault Tree: Backup System Failure6.2 Processing Steps
Step 1: MEF Object Creation
Model* model = new Model();
FaultTree* ft1 = new FaultTree("ECCS-System-Failure");
FaultTree* ft2 = new FaultTree("Containment-Failure");
FaultTree* ft3 = new FaultTree("Backup-System-Failure");
Gate* top1 = new Gate("top1");
Gate* top2 = new Gate("top2");
Gate* top3 = new Gate("top3");
BasicEvent* A = new BasicEvent("A");
BasicEvent* B = new BasicEvent("B");
// ... other eventsStep 2: Formula Creation
top1->formula(AND(OR(A,B), OR(C,D)));
top2->formula(OR(E,F));
top3->formula(AND(G,H));Step 3: PDAG Construction
PDAG 1: Constructed from TOP1's formula: AND(OR(A,B), OR(C,D))
PDAG 2: Constructed from TOP2's formula: OR(E,F)
PDAG 3: Constructed from TOP3's formula: AND(G,H)Step 4: Preprocessing (Phase 1-5)
Phase 1: Basic cleanup
Phase 2: Structural optimization
Phase 3: Full normalization (already normalized)
Phase 4: Complement propagation (no complements)
Phase 5: Final optimizationStep 5: Event Tree Integration
Integrated PDAG:
TOP (System Failure)
├── Sequence 1: ECCS_Available = TRUE
│ ├── AND(OR(A,B), OR(C,D)) // ECCS System Failure
│ └── OR(E,F) // Containment Failure
└── Sequence 2: ECCS_Available = FALSE
└── AND(G,H) // Backup System FailureStep 6: Analysis
// Calculate sequence probabilities
double seq1_prob = P(AND(OR(A,B), OR(C,D))) * P(OR(E,F));
double seq2_prob = 0; // ECCS_Available = FALSE eliminates this sequence
// Total risk
double total_risk = seq1_prob + seq2_prob = seq1_prob;Conclusion
This workflow demonstrates how SCRAM transforms high-level risk models through MEF Formulas into PDAGs for probabilistic analysis. The MEF Formula layer is essential as it provides validation, enables complex logic transformations, and serves as the bridge between the declarative model structure and the computational analysis structure.
