Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pyfair/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""PyFair is an open source implementation of the FAIR methodology."""

VERSION = '0.1-alpha.12'
from ._version import __version__


from . import model
Expand Down
1 change: 1 addition & 0 deletions pyfair/_version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__version__ = "0.1-alpha.12"
34 changes: 12 additions & 22 deletions pyfair/model/model_calc.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,17 @@ class FairCalculations(object):
3) a multiplication function.

"""

def __init__(self):
# Lookup table for functions (no leaf nodes required)
self._function_dict = {
'Risk' : self._calculate_multiplication,
'Loss Event Frequency' : self._calculate_multiplication,
'Threat Event Frequency': self._calculate_multiplication,
'Vulnerability' : self._calculate_step_average,
'Loss Magnitude' : self._calculate_addition,
'Primary Loss' : self._calculate_multiplication,
'Secondary Loss' : self._calculate_multiplication,
"Risk": self._calculate_multiplication,
"Loss Event Frequency": self._calculate_multiplication,
"Threat Event Frequency": self._calculate_multiplication,
"Vulnerability": self._calculate_step_average,
"Loss Magnitude": self._calculate_addition,
"Primary Loss": self._calculate_multiplication,
"Secondary Loss": self._calculate_multiplication,
}

def calculate(self, parent_name, child_1_data, child_2_data):
Expand Down Expand Up @@ -58,22 +59,11 @@ def calculate(self, parent_name, child_1_data, child_2_data):
return calculated_result

def _calculate_step_average(self, child_1_data, child_2_data):
"""Get bool series based on step function, then average for vuln"""
"""Return per-simulation boolean (as float) for Vulnerability: 1.0 if TC > CS, else 0.0"""
# Get Trues (1) where child_2 (TCap) is greater than child_1 (CS)
# Otherwise False (0)
bool_series = child_1_data < child_2_data
# Treat those bools as 1 and 0 and get mean
bool_scalar_average = bool_series.mean()
# Create a long array of that mean
vuln_data = np.full(
len(bool_series),
bool_scalar_average
)
# And put it in a series
vuln = pd.Series(
data=vuln_data,
index=bool_series.index
)
bool_series = (child_1_data < child_2_data).astype(float)
# Return the per-simulation result as a Series
vuln = pd.Series(data=bool_series.values, index=bool_series.index)
return vuln

def _calculate_addition(self, child_1_data, child_2_data):
Expand Down
95 changes: 55 additions & 40 deletions pyfair/model/model_input.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,26 +29,33 @@ class FairDataInput(object):
is stored when converting to JSON or another serialization format.

"""

def __init__(self):
# These targets must be less than or equal to one
self._le_1_targets = ['Probability of Action', 'Vulnerability', 'Control Strength', 'Threat Capability']
self._le_1_keywords = ['constant', 'high', 'mode', 'low', 'mean']
self._le_1_targets = [
"Probability of Action",
"Vulnerability",
"Control Strength",
"Threat Capability",
"Secondary Loss Event Frequency",
]
self._le_1_keywords = ["constant", "high", "mode", "low", "mean"]
# Parameter map associates parameters with functions
self._parameter_map = {
'constant': self._gen_constant,
'high' : self._gen_pert,
'mode' : self._gen_pert,
'low' : self._gen_pert,
'gamma' : self._gen_pert,
'mean' : self._gen_normal,
'stdev' : self._gen_normal,
"constant": self._gen_constant,
"high": self._gen_pert,
"mode": self._gen_pert,
"low": self._gen_pert,
"gamma": self._gen_pert,
"mean": self._gen_normal,
"stdev": self._gen_normal,
}
# List of keywords with function keys
self._required_keywords = {
self._gen_constant: ['constant'],
self._gen_pert : ['low', 'mode', 'high'],
self._gen_normal : ['mean', 'stdev'],
}
self._gen_constant: ["constant"],
self._gen_pert: ["low", "mode", "high"],
self._gen_normal: ["mean", "stdev"],
}
# Storage of inputs
self._supplied_values = {}

Expand All @@ -59,7 +66,7 @@ def get_supplied_values(self):
-------
dict
A dictionary of the values supplied to generate function. The
keys for the dict will be the target node as a string (e.g.
keys for the dict will be the target node as a string (e.g.
'Loss Event Frequency') and the values will be a sub-dictionary
of keyword arguments ({'low': 50, 'mode}: 51, 'high': 52}).

Expand All @@ -80,7 +87,11 @@ def _check_le_1(self, target, **kwargs):
pass
# If not, raise error
else:
raise FairException('"{}" must have "{}" value between zero and one.'.format(target, key))
raise FairException(
'"{}" must have "{}" value between zero and one.'.format(
target, key
)
)

def _check_parameters(self, target_function, **kwargs):
"""Runs parameter checks
Expand All @@ -94,7 +105,7 @@ def _check_parameters(self, target_function, **kwargs):
for keyword, value in kwargs.items():
# Two conditions
value_is_less_than_zero = value < 0
keyword_is_relevant = keyword in ['mean', 'constant', 'low', 'mode', 'high']
keyword_is_relevant = keyword in ["mean", "constant", "low", "mode", "high"]
# Test conditions
if keyword_is_relevant and value_is_less_than_zero:
raise FairException('"{}" is less than zero.'.format(keyword))
Expand All @@ -104,7 +115,11 @@ def _check_parameters(self, target_function, **kwargs):
if required_keyword in kwargs.keys():
pass
else:
raise FairException('"{}" is missing "{}".'.format(str(target_function), required_keyword))
raise FairException(
'"{}" is missing "{}".'.format(
str(target_function), required_keyword
)
)

def generate(self, target, count, **kwargs):
"""Executes request, records parameters, and return random values
Expand All @@ -123,7 +138,7 @@ def generate(self, target, count, **kwargs):
The number of random numbers generated (or alternatively, the
length of the Series returned).
**kwargs
Keyword arguments with one of the following values: {`mean`,
Keyword arguments with one of the following values: {`mean`,
`stdev`, `low`, `mode`, `high`, `gamma`, or `constant`}.

Raises
Expand All @@ -146,8 +161,8 @@ def generate(self, target, count, **kwargs):
result = self._generate_single(target, count, **kwargs)
# Explicitly insert optional keywords for model storage
dict_keys = kwargs.keys()
if 'low' in dict_keys and 'gamma' not in dict_keys:
kwargs['gamma'] = 4
if "low" in dict_keys and "gamma" not in dict_keys:
kwargs["gamma"] = 4
# Record and return
self._supplied_values[target] = {**kwargs}
return result
Expand Down Expand Up @@ -192,16 +207,16 @@ def generate_multi(self, prefixed_target, count, kwargs_dict):

{
'Reputational': {
'Secondary Loss Event Frequency': {'constant': 4000},
'Secondary Loss Event Frequency': {'constant': 4000},
'Secondary Loss Event Magnitude': {
'low': 10, 'mode': 20, 'high': 100
},
},
'Legal': {
'Secondary Loss Event Frequency': {'constant': 2000},
'Secondary Loss Event Frequency': {'constant': 2000},
'Secondary Loss Event Magnitude': {
'low': 10, 'mode': 20, 'high': 100
},
},
}
}

Expand Down Expand Up @@ -242,7 +257,7 @@ def generate_multi(self, prefixed_target, count, kwargs_dict):

"""
# Remove prefix from target
final_target = prefixed_target.lstrip('multi_')
final_target = prefixed_target.lstrip("multi_")
# Create a container for dataframes
df_dict = {target: pd.DataFrame() for target in kwargs_dict.keys()}
# For each target
Expand All @@ -255,9 +270,9 @@ def generate_multi(self, prefixed_target, count, kwargs_dict):
# Put in dict
df_dict[target][column] = s
# Get partial secondary losses and sum up all the values
summed = np.sum(df.prod(axis=1) for df in df_dict.values())
summed = sum(df.prod(axis=1) for df in df_dict.values())
# Record params
new_target = 'multi_' + final_target
new_target = "multi_" + final_target
self._supplied_values[new_target] = kwargs_dict
return summed

Expand Down Expand Up @@ -294,12 +309,12 @@ def supply_raw(self, target, array):
s = pd.Series(clean_array)
# Check numeric and not null
if s.isnull().any():
raise FairException('Supplied data contains null values')
raise FairException("Supplied data contains null values")
# Ensure values are appropriate
if target in self._le_1_targets:
if s.max() > 1 or s.min() < 0:
raise FairException(f'{target} data greater or less than one')
self._supplied_values[target] = {'raw': s.values.tolist()}
raise FairException(f"{target} data greater or less than one")
self._supplied_values[target] = {"raw": s.values.tolist()}
return s.values

def _determine_func(self, **kwargs):
Expand All @@ -309,24 +324,22 @@ def _determine_func(self, **kwargs):
if key not in self._parameter_map.keys():
raise FairException('"{}"" is not a recognized keyword'.format(key))
# Check whether all keys go to same function via set comprension
functions = list(set([
self._parameter_map[key]
for key
in kwargs.keys()
]))
functions = list(set([self._parameter_map[key] for key in kwargs.keys()]))
if len(functions) > 1:
raise FairException('"{}" mixes incompatible keywords.'.format(str(kwargs.keys())))
raise FairException(
'"{}" mixes incompatible keywords.'.format(str(kwargs.keys()))
)
else:
function = functions[0]
return function

def _gen_constant(self, count, **kwargs):
"""Generates constant array of size `count`"""
return np.full(count, kwargs['constant'])
return np.full(count, kwargs["constant"])

def _gen_normal(self, count, **kwargs):
"""Geneates random normally-distributed array of size `count`"""
normal = scipy.stats.norm(loc=kwargs['mean'], scale=kwargs['stdev'])
normal = scipy.stats.norm(loc=kwargs["mean"], scale=kwargs["stdev"])
rvs = normal.rvs(count)
return rvs

Expand All @@ -340,10 +353,12 @@ def _gen_pert(self, count, **kwargs):
def _check_pert(self, **kwargs):
"""Does the work of ensuring BetaPert distribution is valid"""
conditions = {
'mode >= low' : kwargs['mode'] >= kwargs['low'],
'high >= mode' : kwargs['high'] >= kwargs['mode'],
"mode >= low": kwargs["mode"] >= kwargs["low"],
"high >= mode": kwargs["high"] >= kwargs["mode"],
}
for condition_name, condition_value in conditions.items():
if condition_value == False:
err = 'Param "{}" fails PERT requirement "{}".'.format(kwargs, condition_name)
err = 'Param "{}" fails PERT requirement "{}".'.format(
kwargs, condition_name
)
raise FairException(err)
Loading