Packages
FaultTree is a library for performing [fault tree analysis](https://en.wikipedia.org/wiki/Fault_tree_analysis). It includes a small HTTP server capable of graphing the resulting FTA, or returning it as JSON.
Current section
Files
Jump to
Current section
Files
lib/fault_tree.ex
defmodule FaultTree do
@moduledoc """
Main module for creating and interacting with fault trees.
"""
use TypedStruct
require Logger
alias FaultTree.Node
typedstruct do
field :next_id, integer(), default: 0
field :nodes, list(Node.t()), default: []
end
@type error_type :: {:error, String.t()}
@type result :: t() | error_type()
@doc """
Create a new fault tree with an `OR` gate as the root.
"""
@spec create() :: t()
def create(), do: create(:or)
@doc """
Create a new fault tree with the passed in `Node` as the root.
"""
@spec create(Node.t()) :: t()
def create(root = %Node{}) do
%FaultTree{next_id: root.id + 1, nodes: [root]}
end
@doc """
Create a new fault tree and generate a node of the given type for the root.
"""
@spec create(atom) :: t()
def create(root_type) when root_type != :basic do
%Node{id: 0, name: "root", type: root_type}
|> create()
end
@doc """
Add a node to the fault tree. Some validations are performed to make sure the node can
logically be added to the tree.
"""
@spec add_node(FaultTree.t(), Node.t()) :: result()
def add_node(tree, node) do
id = tree.next_id
node = node |> Map.put(:id, id)
case validate_node(tree, node) do
{:error, msg} ->
Logger.error(msg)
{:error, :invalid}
{:ok, tree} ->
tree
|> Map.put(:next_id, id + 1)
|> Map.update!(:nodes, fn nodes -> [node | nodes] end)
end
end
@doc """
Add a basic node to the fault tree with a pre-defined probability.
"""
def add_basic(tree, probability, name), do: add_basic(tree, nil, probability, name, nil)
def add_basic(tree, parent, probability, name), do: add_basic(tree, parent, probability, name, nil)
def add_basic(tree, parent, probability, name, description) do
node = %Node{type: :basic, name: name, probability: Decimal.new(probability),
parent: parent, description: description}
add_node(tree, node)
end
@doc """
Add a logic gate to the fault tree.
"""
def add_logic(tree, parent, type, name, description \\ nil) do
node = %Node{type: type, name: name, parent: parent, description: description}
add_node(tree, node)
end
@doc """
Add an OR gate to the fault tree. Any child nodes failing will cause this node to fail.
"""
def add_or_gate(tree, parent, name, description \\ nil), do: add_logic(tree, parent, :or, name, description)
@doc """
Add an AND gate to the fault tree. All children must fail for this node to fail.
"""
def add_and_gate(tree, parent, name, description \\ nil), do: add_logic(tree, parent, :and, name, description)
@doc """
Add an ATLEAST/VOTING gate to the fault tree. This rqeuires that a minimum of K out of N child nodes fail in
order to be marked as failing.
"""
def add_atleast_gate(tree, parent, min, total, name, description \\ nil) do
node = %Node{type: :atleast, name: name, parent: parent, description: description, atleast: {min, total}}
add_node(tree, node)
end
@doc """
Add a transfer node. This is a reference to a node that already exists in the tree. Transfer nodes cannot have anything modified,
changes must happen on the source.
"""
def add_transfer(tree, parent, source) do
node = %Node{type: :transfer, source: source, name: source, parent: parent}
add_node(tree, node)
end
@doc """
Perform some validation for a new node against the existing tree.
"""
def validate_node(tree, node) do
with {:ok, tree} <- validate_parent(tree, node),
{:ok, tree} <- validate_probability(tree, node),
{:ok, tree} <- validate_atleast(tree, node),
{:ok, tree} <- validate_transfer(tree, node),
{:ok, tree} <- validate_name(tree, node) do
{:ok, tree}
else
err -> err
end
end
@doc """
Validate that the name of the node is unique in the fault tree.
"""
def validate_name(tree, %{type: :transfer, name: name, source: source}) when name == source, do: {:ok, tree}
def validate_name(tree, %{name: new_name}) do
case Enum.find(tree.nodes, fn %{name: name} -> name == new_name end) do
nil -> {:ok, tree}
_ -> {:error, "Name already exists in the tree"}
end
end
@doc """
Validate that the gate types allow setting this node as a child of its listed parent.
"""
def validate_parent(tree, %Node{parent: nil}), do: {:ok, tree}
def validate_parent(tree, node) do
parent = find_by_field(tree, :name, node.parent)
case parent do
nil -> {:error, "Parent not found in tree"}
%Node{type: :basic} -> {:error, "Basic nodes cannot have children"}
%Node{type: :transfer} -> {:error, "Transfer nodes cannot have children"}
%Node{type: :atleast} ->
case {node.type, find_children(parent, tree.nodes)} do
{_, []} -> {:ok, tree}
{:transfer, children} ->
case Enum.filter(children, fn %{name: name} -> name != node.name end) do
[] -> {:ok, tree}
_ -> {:error, "ATLEAST gates can only have a single child node"}
end
{_, _} -> {:error, "ATLEAST gates can only have a single child node"}
end
_ -> {:ok, tree}
end
end
@doc """
Validate that a probability is only set on basic nodes.
Logic gates will have their probability calculated when the tree is built.
"""
def validate_probability(tree, node) do
case node do
%Node{type: :basic, probability: p} when p > 0 and p != nil -> {:ok, tree}
%Node{type: :basic} -> {:error, "Basic events must have a probability set"}
%Node{probability: p} when p > 0 and p != nil -> {:error, "Only basic events should have a probability set"}
_ -> {:ok, tree}
end
end
@doc """
Validate that ATLEAST gates have their parameters set.
"""
def validate_atleast(tree, node) do
case node do
%Node{type: :atleast, atleast: nil} -> {:error, "ATLEAST gates must have minimum and total set"}
_ -> {:ok, tree}
end
end
@doc """
Validate that TRANSFER gates have a source that exists in the tree.
"""
def validate_transfer(tree, node = %Node{type: :transfer}) do
case find_by_field(tree, :name, node.source) do
nil -> {:error, "Source not found for TRANSFER gate"}
_ -> {:ok, tree}
end
end
def validate_transfer(tree, _node), do: {:ok, tree}
@doc """
Convert a tree to JSON.
"""
@spec to_json(t() | map()) :: String.t()
def to_json(tree = %FaultTree{}), do: tree |> build() |> to_json()
def to_json(tree) do
Poison.encode!(tree)
end
def build(tree), do: FaultTree.Analyzer.process(tree)
@doc """
Convert from a string containing fault tree logic into the tree object.
"""
@spec parse(String.t()) :: t()
def parse(doc), do: FaultTree.Parser.XML.parse(doc)
def find_children(%Node{type: :transfer}, _nodes), do: []
def find_children(node, nodes), do: Enum.filter(nodes, fn x -> x.parent == node.name end)
defp find_by_field(tree, field, value), do: tree.nodes |> Enum.find(fn node -> Map.get(node, field) == value end)
end