%matplotlib inline

"""
Explanation: Alternative Analysis Using the Swing Weight Matrix
An IPython Notebook implementation of the Systems Engineering trade study method described at http://sebokwiki.org/wiki/Decision_Management.
Table of Contents

Import Modules
Declare Variables and Define Functions
Input Files
Score Alternatives
Calculate Swing Weights
Evaluate Alternative Value
Visualize Output
Create HTML Report
End of explanation
"""


from __future__ import division

import numpy as np
import scipy as sp
from scipy.special import expit

import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
sns.set(style = 'whitegrid') # plot aesthetics
# plt.style.use('bmh') # plot aesthetics

import markdown
from jinja2 import Environment, FileSystemLoader

"""
Explanation: Import Modules <a name="Import"></a>
End of explanation
"""


# distribution short names
sigString = 'sigmoid'
linString = 'linear'
powString = 'power'
triString = 'triangular'

# Value function range [0, vfRange]
vfRange = 100.0

# Weights for Importance and Swing for Swing Weight calculation
impWt = 0.65
swWt = 1.0 - impWt

# For large figures with subplots
numPlotCols = 3 # number of columns

# HTML output
htmlReport = True

"""
Explanation: Variable and Function Declarations <a name="VarFunc"></a>
End of explanation
"""


# the linear, or scaled, function
def scale(x, xMin, xMax):
    """ Returns x between [0.0, 1.0] from original domain of [xMin, xMax]. """
    return (x - xMin) / (xMax - xMin)

# the triangular function
def triangular(x, l, c, r):
    """ Returns the [0,100] scaled triangular value function evaluated 
    at x for (l)eft, (c)enter, (r)ight triangular parameters."""
    return vfRange * max(min(((x - l)/(c - l)), ((r - x)/(r - c))), 0.0)

# the bell function
def bell(x, a, b, c):
    """ Returns the [0,100] scaled generalized bell curve 
    evaluated at x for (c)enter and shape parameters a and b."""
    return vfRange * 1.0 / (1.0 + pow(np.abs((x - c)/a), (2.0*b)))

# the sigmoid function
def sigmoid(x, a, c):
    """Returns the [0,100] scaled sigmoid function evaluated 
    at x for (a)lpha and (c)enter."""
    return vfRange * (1.0 / (1.0 + np.exp(-1.0 * a * (x - c))))


"""
Explanation: Define the Allowable Value Functions
End of explanation
"""


objDF = pd.read_csv('./input/objectives.csv', index_col = ['Objective', 'Measure'])
# objDF   # Uncomment to view the DataFrame inline

"""
Explanation: Read Input Files <a name="Input"></a>
Objectives and Measures
Read directly into a pandas DataFrame
Define the set of value function families to use:
Family  |     Value Function Form
------- | -------------------------------------------
Linear  |  $$ f(x) = mx + b $$
Power   |  $$ f(x) = mx^a $$
Sigmoid |  $$ f(x) = \frac{a}{b + e^{-ax/2}} $$
End of explanation
"""


tmpDF = pd.DataFrame(columns=['Measure', 'Score', 'Value'])

# Get the number of unique Measures to plot
numPlotRows = int(round(np.ceil(len(objDF.index.levels[1].unique()) / numPlotCols), 0))
fig, axs = plt.subplots(numPlotRows, numPlotCols, figsize = (13, 15))

# Loop through the subplots and objDF indices
for ax, idx in zip(axs.flat, objDF.index):
    vals = objDF.loc[idx] # get the dataframe columns for this index 
    axMin = float(vals.Minimum)
    axMax = float(vals.Maximum)
    domain = np.linspace(axMin, axMax)
    
    # Build the corresponding value function
    if vals.Family == 'sigmoid':
        valFunc = [sigmoid(i, float(vals.Param1), vals.Param2) for i in domain]
        if float(vals.Slope) == -1.0:
            valFunc[:] = [vfRange - i for i in valFunc]
    elif vals.Family == 'linear':
        valFunc = [vfRange * scale(i, axMin, axMax) for i in domain]
        if float(vals.Slope) == -1.0:
            valFunc[:] = [vfRange - i for i in valFunc]
    elif vals.Family == 'power':
        valFunc = [vfRange * np.power(scale(i, axMin, axMax), vals.Param1) for i in domain]
        if float(vals.Slope) == -1.0:
            valFunc[:] = [max(valFunc) - i for i in valFunc]
    elif vals.Family == 'triangular':
        valFunc = [triangular(i, axMin, vals.Param1, axMax) for i in domain]
        if float(vals.Slope) == -1.0:
            valFunc[:] = [max(valFunc) - i for i in valFunc]
    else:
        valsFunc = [0]*len(domain)
        print 'This value function family is not yet implemented.'
    
    tmpDF.Measure = str(idx[1]) # Assign the Measure
    tmpDF.Score = domain
    tmpDF.Value = valFunc
    
    plotTitle = str(idx[1])
    
    tmpDF.plot(ax=ax, x = 'Score', y = 'Value', title = plotTitle, legend = False)
    plt.subplots_adjust(hspace = 0.7)
    
    
for ax in axs.flat[axs.size - 1:len(objDF.index) - 1:-1]:
    ax.set_visible(False)


plt.suptitle('Value Functions', fontsize = 16)
plt.savefig('./html_report/images/value-functions.png', bbox_inches='tight')
plt.savefig('./html_report/images/value-functions.pdf', bbox_inches='tight')

plt.show()

"""
Explanation: Build the Value Functions for Each Objective and Measure
This section creates plots of the value functions defined for each measure.  The result is a series of figure with subplots of each value function.

Note: If you defined additional value functions above, you will need to add these to the below loop to make sure they are evaluated.
End of explanation
"""


altDF = pd.read_csv('./input/alternatives.csv', index_col=['Objective', 'Measure', 'Alternative'])


# Create and intialize a columns for Consequence
altDF['Consequence'] = np.NaN
# altDF   #  Uncomment to view inline


"""
Explanation: Alternatives
Read directly into a pandas DataFrame
End of explanation
"""


for item, val in altDF.iterrows():
    idxString = list(item) # convert this alternative's index to a list
    print '\n\nidxString is: ', idxString
    print 'val is: \n -----\n', val, '\n-----'
    objIdx = idxString[0:2] # drop the 'Alternative' from the index used for the objective DataFrame
    print 'objective index is: \n', objIdx
    obj = objDF.ix[objIdx[0],objIdx[1],] # get the corresponding objective for this index
    print 'objDF row is: \n', obj
    
    score = np.NaN
    funcFamily = str(obj['Family']) # get the corresponding value function family
    paramX = altDF.ix[idxString[0], idxString[1], idxString[2]]['Score'] # the measured value for this alternative
    paramSlope = objDF.ix[objIdx[0],objIdx[1]]['Slope'] # the slope
    paramXMin = objDF.ix[objIdx[0],objIdx[1]]['Minimum'] # minimum acceptable
    paramXMax = objDF.ix[objIdx[0],objIdx[1]]['Maximum'] # maximum desirable
    paramX1 = objDF.ix[objIdx[0],objIdx[1]]['Param1'] # 1st function parameter
    paramX2 = objDF.ix[objIdx[0],objIdx[1]]['Param2'] # 2nd function parameter
    paramX3 = objDF.ix[objIdx[0],objIdx[1]]['Param3'] # 3rd function parameter

    if funcFamily == sigString:
        print str(idxString[0:2]) + ' is ' + sigString
        score = sigmoid(paramX, paramX1, paramX2)
        if float(paramSlope) == -1.0:
            score = vfRange - score
    elif funcFamily == linString:
        print str(idxString[0:2]) + ' is ' + linString
        score = vfRange * scale(paramX, paramXMin, paramXMax)
        if float(paramSlope) == -1.0:
            score = vfRange - score
    elif funcFamily == powString:
        print str(idxString[0:2]) + ' is ' + powString
        score = np.power(scale(paramX, paramXMin, paramXMax), paramX1) #pow_func(paramX, 2.0, paramXMin, paramXMax)
        if float(paramSlope) == -1.0:
            score = vfRange - score
    elif funcFamily == triString:
        print str(idxString[0:2]) + ' is ' + triString
        score = triangular(paramX, paramXMin, paramX1, paramXMax)
        if float(paramSlope) == -1.0:
            score = vfRange - score
    else:
        print 'The "',funcFamily, '" value function family is not yet implemented.'
        
    if score > 100.0:
        score = 100.0
    elif score < 0.0:
        score = 0.0

    print 'Value against this measure is ', score
    altDF.loc[(idxString[0], idxString[1], idxString[2]),'Consequence'] = np.round(float(score), 3)
                  


print altDF['Score'].dropna('index')   # Uncomment to view the DataFrame inline

"""
Explanation: Score each Alternative against each Objective and Measure <a name="AltScoring"></a>
End of explanation
"""


objDF.loc[:,'Swing'] = np.NaN

for idx, val in objDF.iterrows():
    try:
        objMin = min(altDF.loc[(idx[0], idx[1]),'Consequence'])
        objMax = max(altDF.loc[(idx[0], idx[1]),'Consequence'])
    except:
        print "\nNo Score found for ", idx
        objMin = np.NaN
        objMax = np.NaN
        
    swing = objMax - objMin
    objDF.loc[(idx[0], idx[1]),'Swing'] = swing
    print idx, ' min: ', objMin, '  max: ', objMax, '  swing: ', swing

# objDF

objResults = objDF.copy()
objResults.reset_index(inplace=True)

objResults


# sns.set(style='whitegrid')
sns.lmplot(x="Swing", y="Importance", data=objResults, fit_reg=False, hue = "Measure", aspect=1.3, 
           scatter_kws={"s": 100}, palette=sns.color_palette("Paired", n_colors=16, desat=.5))#palette='Accent')
plt.xlim(-0.1,101)
plt.ylim(-0.1,101)
plt.title("Measure Swing vs. Importance")
plt.savefig('./html_report/images/swing-importance.png', bbox_inches='tight')
plt.savefig('./html_report/images/swing-importance.pdf', bbox_inches='tight')

"""
Explanation: Calculate the Swing Weight for each Objective and Measure <a name="CalcSW"></a>
End of explanation
"""


objDF.loc[:,'Weight'] = impWt * objDF.Importance + swWt * objDF.Swing

"""
Explanation: Calculate the Unnormalized Weight
End of explanation
"""


objDF.loc[:,'NormdWt'] = objDF.loc[:,'Weight'] / objDF.loc[:,'Weight'].sum()

# objDF   # Uncomment to view the DataFrame inline

"""
Explanation: Calculate the Normalized Weight
End of explanation
"""


for idx, vals in altDF.iterrows():
    altDF.loc[:,'WtdConsequence'] = objDF.loc[(idx[0],idx[1]),'NormdWt'] * altDF.Consequence


# altDF    # Uncomment to view the DataFrame inline
print altDF['WtdConsequence'].dropna('index')

"""
Explanation: Evaluating the Alternative's Value <a name="AltValue"></a>
Calculate Total Value for Each Alternative
$$
V(x) = \sum_{i = 1}^{n}w_{i} v_{i}(x_{i})
$$
where $V(x)$ is the total value, $i$ is the index of the objective/measure, $w_i$ is the normalized weight for objective/measure $i$, $x_i$ is the alternative's score for objective measure $i$, and $v_i(x_i)$ is the corresponding value of $x_i$.
End of explanation
"""


heatDF = altDF.drop(['Score', 'Units', 'WtdConsequence'], axis = 1)
heatDF.reset_index(inplace=True)

summaryDF = altDF.drop(['Score', 'Consequence'], axis=1).groupby(level = 'Alternative').agg(sum)
summaryDF.columns = ['Value']
# summaryDF   # Uncomment to view the DataFrame inline

tmpDF = altDF.drop(['Units', 'Consequence'], axis = 1)
# tmpDF   # Uncomment to view the DataFrame inline

for idx, cols in tmpDF.iterrows():
    val = summaryDF.loc[(idx[2]), 'Value']
    tmpDF.loc[idx,'Value'] = val


heat_rect = heatDF.pivot('Alternative', 'Measure', 'Consequence')
# heat_rect.dropna("columns")   # Uncomment to view the DataFrame inline

heat_rect

sns.heatmap(np.round(heat_rect.dropna('columns'), 0), annot=True, fmt='g', cbar=False)
plt.title("Heatmap of Alternative Measure Values")
plt.savefig('./html_report/images/value-scorecard.png', bbox_inches='tight')
plt.savefig('./html_report/images/value-scorecard.pdf', bbox_inches='tight')

"""
Explanation: Visualizing Output <a name="Viz"></a>
Heatmap (Consequences Scorecard)
Display the relative performance of each Alternative against each Measure.
End of explanation
"""


summaryDF.reset_index(inplace=True)

sns.barplot('Alternative', 'Value', data=summaryDF, palette='muted')
plt.ylabel("Value")
plt.title('Total Alternative Value')
plt.savefig('./html_report/images/value-barplot.png', bbox_inches='tight')
plt.savefig('./html_report/images/value-barplot.pdf', bbox_inches='tight')

summaryDF

tmpDF.reset_index(inplace=True)

grid = sns.FacetGrid(tmpDF, col="Measure", hue="Alternative", col_wrap=3, size=4, legend_out = True, sharex=False, 
                    sharey=True)
grid.map(plt.plot, "Score", "Value", marker="o", ms=14, alpha=0.6)
grid.fig.tight_layout(w_pad=1)
sns.set_context("paper", font_scale=1.6)
grid.add_legend()
grid.savefig('./html_report/images/measure-trellis.png', bbox_inches='tight')
grid.savefig('./html_report/images/measure-trellis.pdf', bbox_inches='tight')

"""
Explanation: Trellis Plot of Alternative Value vs Measure Score
Allows a quick comparison of the total value and original score of Alternatives against all Measures.
End of explanation
"""


fooDF = heat_rect.copy()

# fooDF

# normalize the measure values for the parallel coordinates plot
for col in fooDF.columns:
    fooDF[col] = (fooDF[col] - np.min(fooDF[col])) / (np.max(fooDF[col]) - np.min(fooDF[col]))

fooDF

fooDF = fooDF.reset_index(level=0,drop = False) # reset the index, but keep 'Alternative' as a column

fooDF.index.name = None

plt.figure(figsize = (15,6))
pd.tools.plotting.parallel_coordinates(fooDF, 'Alternative', colormap = 'Set2')
plt.xticks(rotation=60)
plt.ylabel("Normalized Performance")
plt.title('Alternative Performance vs Measure')
plt.legend(loc='center left', bbox_to_anchor=(1, 0.5))
plt.savefig('./html_report/images/parallel-coordinates.png', bbox_inches='tight')
plt.savefig('./html_report/images/parallel-coordinates.pdf', bbox_inches='tight')

"""
Explanation: Format the DataFrame to produce the parallel coordinates plot.
End of explanation
"""


if htmlReport:
    # Adjust these strings to your needs:
    pageTitle = "alt-swing"
    reportTitle = "Default alt-swing Report"
    author = "Author Name"
    frontMatter = "This is the default alt-swing report.  You can customize it for your needs."
    content = "Lorem ipsum dolor sit amet, consectetur adipiscing elit."
    trellis_text = "Nunc vel gravida dui, ac aliquam augue. Vivamus eu ultrices mauris, sit amet dictum diam."
    barplot_text = "Vivamus eu ultrices dui, ac aliquam augue."
    
    # Convert tables
    summaryDF = altDF.drop(['Score', 'Consequence'], axis=1).groupby(level = 'Alternative').agg(sum)
    summaryDF.columns = ['Value']
    
    altTable = pd.read_csv('./input/alternatives.csv', index_col=['Objective', 'Measure', 'Alternative', 'Units'])
    altTable = altTable.unstack(['Objective', 'Measure', 'Units'])
    
    blufHTML = summaryDF.to_html()\
                .replace('<table border="1" class="dataframe">','<table border="0" style="font-size:80%"">')
    
    altHTML = altTable.to_html()\
                .replace('<table border="1" class="dataframe">','<table border="0" style="font-size:80%">')
    
    objHTML = objDF.drop(['Family', 'Slope', 'Param1', 'Param2', 'Param3', 'Weight'], axis=1).to_html()\
                .replace('<table border="1" class="dataframe">','<table border="0" style="font-size:80%">')
    
    # Read and convert markdown sections
    with open('./templates/content/intro-section.md', 'r') as f:
        introText = markdown.markdown(f.read())
    
    with open('./templates/content/bluf-section.md', 'r') as f:
        blufText = markdown.markdown(f.read())
        
    with open('./templates/content/obj_meas-section.md', 'r') as f:
        obj_measText = markdown.markdown(f.read())

    with open('./templates/content/alternative-section.md', 'r') as f:
        altText = markdown.markdown(f.read())
    
    env = Environment(loader=FileSystemLoader('./templates/'))
    template = env.get_template('template.html')
    
    # 
    output_from_parsed_template = template.render(
        title = reportTitle, page_title = pageTitle, author = author,
        bluf_content = blufText, bluf_table = blufHTML, 
        intro_content = introText, 
        obj_content = obj_measText, obj_meas_table = objHTML,
        alt_content = altText, alt_table = altHTML, 
        trellis_text = trellis_text, barplot_text = barplot_text
        )

    output_from_parsed_template = output_from_parsed_template.encode("utf-8")
    
    # Write the HTML report
    with open("./html_report/alt-swing_report.html", "w") as fh:
        fh.write(output_from_parsed_template)
    


"""
Explanation: Create HTML Report <a name="Report"></a>
You can manipulate the string variable below for your needs.  These are used in the default template.
End of explanation
"""


