# Copyright 2025, Battelle Energy Alliance, LLC ALL RIGHTS RESERVED
import numpy as np
import logging
from scipy.stats import truncnorm
from bahamas.sdlc_stage_hep_calculation import sdlc_stage_hep_calculation
from bahamas.sdlc_stage_hep_calculation_approx import sdlc_stage_hep_calculation_approx
from bahamas.human_error_mode_distribution import get_hemd_from_spreadsheet
from bahamas.stage_odc_distribution import get_stage_odc_dist
from bahamas.defect_conditional_probability import stage_dcp_calculation
from bahamas.uca_defect_correlation import get_uca_defect_correlation_dist
from bahamas.utils import SDLC_stages, ODC_types, UCA_types
from bahamas.plot_utils import plot_histogram
[docs]
logger = logging.getLogger('BAHAMAS.BBN')
[docs]
class BBN(object):
"""
Bayesian belief network for reliability analysis of software
"""
def __init__(self, defect_file, task_file, num_samples=1000, approx=False, data=None, seed=42):
[docs]
self.num_samples = num_samples
np.random.seed(seed)
[docs]
self._sdlc = SDLC_stages
[docs]
self._defect = defect_file
[docs]
self.prob_stage = {} # HEP for each stage
[docs]
self.prob_odc = {} # ODC marginal probability
[docs]
self.prob_dcp = {} # DEP for each stage
[docs]
self.prob_total = None # Software total probability
[docs]
self.prob_uca = {} # UCA probability
if not self._approx:
_, self._hemd_dist = get_hemd_from_spreadsheet(self._defect)
[docs]
self.prob_stage_odc = get_stage_odc_dist(self._defect)
[docs]
self.prob_uca_correlation = get_uca_defect_correlation_dist(self._defect)
[docs]
self.review_trigger_factor = np.exp(-8)*self._G # The same as the value used in Software_Quality_Survey.py
[docs]
def initialize_stage(self):
G = self._G
for stage, vals in self._data.items():
rev = vals['review']
trigger = vals['trigger']
if 'samples' not in vals.keys():
mean = vals['mean']
std = vals['std']
a = (0 - mean) / std
b = (1 - mean) / std
dist = truncnorm(a, b, loc=mean, scale=std)
total = dist.rvs(size=self.num_samples)
self.prob_stage[stage] = total
self.prob_dcp[stage] = G*np.exp(-4.*rev*trigger)
else:
self.prob_stage[stage] = vals['samples']
self.prob_dcp[stage] = G*np.exp(-4.*rev*trigger)/self.review_trigger_factor
[docs]
def calculate(self):
"""Calculate software failure probability based on BBN
"""
logger.info('Sampling HEP and DCP')
for stage in self._sdlc:
if self._approx:
if self._task is not None:
# calculate human error propagation for each SDLC stage [sampled values]
self.prob_stage[stage], _ = sdlc_stage_hep_calculation_approx(self._task, stage, self.num_samples, distribution="norm")
# calculate DCP for each SDLC stage [single value without sampling]
self.prob_dcp[stage] = stage_dcp_calculation(self._task, stage)
else:
if self._task is None:
raise IOError("Task List input file is requested, but missing!")
# calculate human error propagation for each SDLC stage [sampled values]
self.prob_stage[stage], _ = sdlc_stage_hep_calculation(self._task, stage, self._hemd_dist, self.num_samples)
# calculate DCP for each SDLC stage [single value without sampling]
self.prob_dcp[stage] = stage_dcp_calculation(self._task, stage)
if len(self.prob_stage) == 0 and self._data is not None:
self.initialize_stage()
# Sample stage ODC conditional probability
logger.info('Sampling ODC')
prob_stage_odc = {}
for stage in self._sdlc:
prob_stage_odc[stage] = {}
for odc in self._odc:
prob_stage_odc[stage][odc] = self.prob_stage_odc[stage][odc].rvs(self.num_samples)
# Sample UCA correlation
logger.info('Sampling UCA')
prob_uca_correlation = {}
for uca in self._uca:
prob_uca_correlation[uca] = {}
for odc in self._odc:
prob_uca_correlation[uca][odc] = self.prob_uca_correlation[uca][odc].rvs(self.num_samples)
# Calculate Marginal Probability
logger.info('Compute marginal ODC')
prob_stage_odc_marg = {}
for stage in self._sdlc:
prob_stage_odc_marg[stage] = {}
for odc in self._odc:
prob_stage_odc_marg[stage][odc] = prob_stage_odc[stage][odc] * self.prob_stage[stage] * self.prob_dcp[stage]
# Propagate uncertainties via BBN
logger.info('BBN Propagation')
level = 2
concept, requirement, design, implementation, testing, InMain = SDLC_stages
for odc in self._odc:
prob = 0
for k in range(level):
for l in range(level):
for m in range(level):
for n in range(level):
for p in range(level):
for q in range(level):
ak = prob_stage_odc_marg[concept][odc] if k == 0 else 1. - prob_stage_odc_marg[concept][odc]
bl = prob_stage_odc_marg[requirement][odc] if l == 0 else 1. - prob_stage_odc_marg[requirement][odc]
cm = prob_stage_odc_marg[design][odc] if m == 0 else 1. - prob_stage_odc_marg[design][odc]
dn = prob_stage_odc_marg[implementation][odc] if n == 0 else 1. - prob_stage_odc_marg[implementation][odc]
ep = prob_stage_odc_marg[testing][odc] if p == 0 else 1. - prob_stage_odc_marg[testing][odc]
fq = prob_stage_odc_marg[InMain][odc] if q == 0 else 1. - prob_stage_odc_marg[InMain][odc]
factor = 0. if np.all([k,l,m,n,p,q]) else 1.
prob += factor * ak * bl * cm * dn * ep * fq
self.prob_odc[odc] = prob
logger.info('Compute UCA and total failure probabilities')
# Evaluate UCA probability and Total probability
self.prob_total = 0.
for uca in self._uca:
prob = 0.
for odc in self._odc:
prob += self.prob_odc[odc] * prob_uca_correlation[uca][odc]
self.prob_uca[uca] = prob
self.prob_total += prob
[docs]
def get_total_failure_probability(self):
"""Get total failure probability
Returns:
tuple: mean, sigma, and samples
"""
mean = np.mean(self.prob_total)
sigma = np.std(self.prob_total)
return mean, sigma, self.prob_total
[docs]
def get_uca(self, uca_type):
"""Get UCA probability
Args:
uca_type (str): Type of UCA
Returns:
tuple: mean, sigma, and samples
"""
prob_uca = self.prob_uca[uca_type]
mean = np.mean(prob_uca)
sigma = np.std(prob_uca)
return mean, sigma, prob_uca
[docs]
def plot(self, type='all', save=False, show=True):
"""Plot calculated data
Args:
type (str, optional): Type of plots to plot (defaults to 'all')
save (bool, optional): Save plots into .png file if True (defaults to False)
Returns:
list or figure object: plotly figure object
"""
fig = None
if type.lower() == 'all':
fig1 = plot_histogram(self.prob_stage, title='Software Development Life Cycle Stage Failure Probabilities Based on Human Error Propagation', save=save, show=show)
fig2 = plot_histogram(self.prob_odc, title='Software ODC Failure Probabilities', save=save, show=show)
fig3 = plot_histogram(self.prob_uca, title='Software UCA Failure Probabilities', save=save, show=show)
fig4 = plot_histogram({'Total Failure Probability':self.prob_total}, title='Total Software Failure Probability', save=save, show=show)
fig = [fig1, fig2, fig3, fig4]
elif type.lower() == 'stage':
fig = plot_histogram(self.prob_stage, title='Software Development Life Cycle Stage Failure Probabilities Based on Human Error Propagation', save=save, show=show)
elif type.lower() == 'odc':
fig = plot_histogram(self.prob_odc, title='Software ODC Failure Probabilities', save=save, show=show)
elif type.lower() == 'uca':
fig = plot_histogram(self.prob_uca, title='Software UCA Failure Probabilities', save=save, show=show)
elif type.lower() == 'total':
fig = plot_histogram({'Total Failure Probability':self.prob_total}, title='Total Software Failure Probability', save=save, show=show)
return fig