# Ising fitter for capped repeat proteins with mutations.

Authors:  Doug Barrick, Jacob D. Marold, Kathryn Geiger-Schuller, Tural Aksel, Ekaterina Poliakova-Georgantas, Sean Klein, Kevin Sforza, Mark Peterson

This notebook reads data from Aviv data files, converts the data to normalized unfolding transitions, generates partition functions and expressions for fraction folded, and uses these expressions to fit the normalized transitions.  Data and fits are plotted in various ways, and bootstrap analysis is performed.  Correlation plots are generated for pairs of bootstrap parameter values.

In this notebook, constructs of NRC type are fitted along with constructs of NXC type, where X repeats are R repeats with point substitutions.  The data analyzed are consensus ankyrin repeets collected by Tural Aksel, along with T4V-substituted consensus ankyrin repeats collected by Kevin Sforza.  The data conversion script takes two csv files, one containing the NRC data and the other containing the T4V data (I don't have the original Aviv .dat files for T4V) and are separated and converted into .npy files for fitting.  This script also generates the constructs.json and melts.json lists.

Here I am fitting two separate m values, one for R and one for X repeats.  This fits better than one m-value.

## Imports, path, and project name

Path and project name should be set by the user.  Note that because of the kernel restart below, these must be specified in subsequent scripts, along with any other imports that are needed.

```python
import numpy as np
import glob
import csv
import pandas as pd
import os
import json
import time
import sympy as sp
import matplotlib.pyplot as plt
from matplotlib.backends.backend_pdf import PdfPages
import lmfit

path = os.getcwd()

proj_name = 'T4V_NRC_2mi'
```

## Data conversion from *two* csv files.

In this script, data are read from two csv files using pandas dataframes.  There is a csv file for the T4V data, and a second for the NRC data.  Imported to dataframes and combined into a final dataframe.
Outputs are

1.  A numpy data file for each melt, contining [denaturant], normalized signal, construct ID, and melt ID.

2.  A list of constructs.

3.  A list of melts.

4.  A combined csv file with all melts (T4V and NRC).

For the lists of constructs and melts, there are loops that attempt to put the constructs and melts in order so that they appear in a logical progression from short to long and depending on repeat type.

```python
den_nsig_const_melt = []
constructs = []         # List of constructs used to build partition functions in next script
melts = []              # List of melts to be used in fitting script

t4v_filepath = os.path.join(path, 'T4Vdata_not_normalized.csv')
nrc_filepath = os.path.join(path, 'NRC_data_dnmn.csv')

T4V_input_df = pd.read_csv(t4v_filepath,names=['denat','signal','construct_melt','dataset'])
NRC_input_df = pd.read_csv(nrc_filepath,names=['denat','signal','construct_melt','dataset'])
maxT4Vmelt = T4V_input_df['dataset'].max()  # Finds the maximum number of melts in the first df
NRC_input_df['dataset'] = NRC_input_df['dataset'] + maxT4Vmelt # Adds the max number to the melt numbers in second df
combined_input_df = pd.concat([T4V_input_df, NRC_input_df],names=['denat','signal','construct_melt','dataset'])
combined_input_df.to_csv(os.path.join(path, 'T4V_NRC_dnmn.csv'), index=False, header=False)

num_melts=combined_input_df['dataset'].max()
for melt in np.arange(num_melts)+1:
    temp_df = combined_input_df.loc[combined_input_df.dataset == melt] # Pulls out just one melt

    # Normalizing the signal
    min = temp_df['signal'].min()
    max = temp_df['signal'].max()
    series=(temp_df['signal'] - min)/(max - min)
    temp_df['signal'] = series  # Overwrites un-normalized signal
    temp_df.rename(columns={'signal': 'nsig'}, inplace=True)
    temp_list = temp_df.values.tolist()
    temp_nparray = np.array(temp_list)
    construct_melt = temp_df.iloc[0, 2]
    np.save(os.path.join(path, construct_melt), temp_nparray) # Writes an npy file to disk for each melt.
    melts.append(construct_melt)

'''
This loop puts melts in order of type (NRxC, NRx, RxC) and length.  This is useful for the
plotting script below, putting the by_melt legends in a sensible order
'''
NRClist = []
NRlist = []
RClist = []
melts.sort()
i = 0
for melt in melts:
    if melt[0] == 'N':
        if melt[-3] == 'C':
            NRClist.append(melt)
        else:
            NRlist.append(melt)
    else:
        RClist.append(melt)

NRClist.sort(key=len)
NRlist.sort(key=len)
RClist.sort(key=len)
melts = NRClist + NRlist + RClist

# Generate a list of just the constructs.  The loop removes duplicates.
for melt in melts:
    if melt[:-2] not in constructs:
        constructs.append(melt[:-2])

with open(os.path.join(path, f"{proj_name}_constructs.json"), 'w') as r:
    json.dump(constructs, r)

with open(os.path.join(path, f"{proj_name}_melts.json"), 'w') as s:
    json.dump(melts, s)

```

## Generate a partition function and fraction folded expressions for fitting.

Inputs the constructs.json, melts.json, and processed .npy data files from the data processing script above.

Generates a dictionary of partition functions using the capped homopolymer 1D-Ising model, and converts these to dictionaries of fraction-folded expressions (**fraction_folded_dict**) for fitting by partial differentiation.  Manipulations are done using the sympy module which allows symbolic math operations.  This is important for partial differentiation, but also for "simplification" of the fraction folded exprssions.  This simplification factors common terms, significantly decreasing the time it takes to fit and bootstrap below.  The fraction-folded dictionary is exported in json format.

Because the numpy exponential function (np.exp) gets reassigned in this script, and I cannot figure out how to undo this, the kernel must be restarted at the bottom of the script (exit()).  The user will be prompted to accept.

Though the path, project name, and most (but not all imports) are redundant with the command above, the kernel restart at the end of this script can create problems, if the script is run more than once.  For this reason I am keeping them associated with this script (and with subsequent scripts--fitting, plotting, etc).

Note that on 2020_05_05, I am changing the equation that gives the denaturant dependence of DGi to DGi + mi denat in the three equations for N, R, and C.  This corresponds to a positive m-value (free energies become more positive with denaturant).  Also change initial guess in the fitting cell.

```python
start = time.time()

print('Generating partition functions and fraction folded expressions.  This may take a minute...')

# Parameters for partition function calculation.  Note these are sympy symbols.
RT = sp.Symbol('RT')
dGN = sp.Symbol('dGN')
dGR = sp.Symbol('dGR')
dGX = sp.Symbol('dGX')
dGC = sp.Symbol('dGC')
dGRR = sp.Symbol('dGRR')
dGXX = sp.Symbol('dGXX')
dGXR = sp.Symbol('dGXR')
dGRX = sp.Symbol('dGRX')
mR = sp.Symbol('mR')
mX = sp.Symbol('mX')
denat = sp.Symbol('denat')
KN = sp.Symbol('KN')
KR = sp.Symbol('KR')
KX = sp.Symbol('KX')
KC = sp.Symbol('KC')
TRR = sp.Symbol('TRR')
TXX = sp.Symbol('TXX')
TXR = sp.Symbol('TXR')
TRX = sp.Symbol('TRX')

exp = sp.Function('np.exp')

with open(os.path.join(path, f"{proj_name}_constructs.json"), 'r') as cons:
    constructs = json.load(cons)

#define weight matricies and end vectors to be used to calculate partition functions
begin = sp.Matrix([[0,1]])
woN = sp.Matrix([[KN,1],[KN,1]]) # Leave off coupling term.  No zeroth repeat
woR = sp.Matrix([[KR,1],[KR,1]]) # Leave off coupling term.  No zeroth repeat
woX = sp.Matrix([[KX,1],[KX,1]]) # Leave off coupling term.  No zeroth repeat

wnR = sp.Matrix([[KR*TRR,1],[KR,1]]) # Treat coupling same as an rR interface, as usual
wrR = sp.Matrix([[KR*TRR,1],[KR,1]]) # Same as wnR above
wxR = sp.Matrix([[KR*TXR,1],[KR,1]])

wnX = sp.Matrix([[KX*TRX,1],[KX,1]]) # Different coupling than
wrX = sp.Matrix([[KX*TRX,1],[KX,1]]) # Different coupling than wnX above
wxX = sp.Matrix([[KX*TXX,1],[KX,1]])

wrC = sp.Matrix([[KC*TRR,1],[KC,1]]) # Treat copuling same as an rR interface, as usual
wxC = sp.Matrix([[KC*TXR,1],[KC,1]])

end = sp.Matrix([[1],[1]])

# Build a dictionary of these matrices
w_dict = {'oN': woN,
         'oR': woR,
         'oX': woX,
         'nR': wnR,
         'rR': wrR,
         'xR': wxR,
         'nX': wnX,
         'rX': wrX,
         'xX': wxX,
         'rC': wrC,
         'xC': wxC}

# Build dictionaries of partition functions, partial derivs with respect
# to K, and fraction folded.

q_dict = {}
dqdKN_dict = {}
dqdKR_dict = {}
dqdKX_dict = {}
dqdKC_dict = {}
frac_folded_dict = {}

# Number of repeats of each type.  Seems like they should be floats, but
# I get an error in the matrix multiplication (q_dict) if they are declared to be.

for construct in constructs:

    # Make partition function dictionary and expressions for fraction folded.
    # Note, only one pf is generated per construct, even when there are multiple melts.

    repeat_list = construct.split('_')
    repeat_list.insert(0, 'o')

    pairs_list = []
    i = 1
    while i < len(repeat_list):
        pair = repeat_list[i-1].lower() + repeat_list[i] # Need to convert prev rept to lower case
        pairs_list.append(pair)
        i = i + 1

    q_dict[construct + '_q'] = begin

    for pair in pairs_list:
        q_dict[construct + '_q'] = q_dict[construct + '_q'] * w_dict[pair]

    q_dict[construct + '_q']  = q_dict[construct + '_q'] * end

    # Next two lines convert from sp.Matrix to np.array to something else.
    # Not sure the logic here, but it works.
    q_dict[construct + '_q'] = np.array(q_dict[construct + '_q'])
    q_dict[construct + '_q'] = q_dict[construct + '_q'].item(0)

    # Partial derivs wrt KN dictionary.
    dqdKN_dict[construct + '_dqdKN'] \
        = sp.diff(q_dict[construct + '_q'], KN)

    # Partial derivs wrt KR dictionary.
    dqdKR_dict[construct + '_dqdKR'] \
        = sp.diff(q_dict[construct + '_q'], KR)

    # Partial derivs wrt KX dictionary.
    dqdKX_dict[construct + '_dqdKX'] \
        = sp.diff(q_dict[construct + '_q'], KX)

    # Partial derivs wrt KC dictionary.
    dqdKC_dict[construct + '_dqdKC'] \
        = sp.diff(q_dict[construct + '_q'], KC)

    # Fraction folded dictionary.
    frac_folded_dict[construct + '_frac_folded'] \
        = (KN/(q_dict[construct + '_q']) * dqdKN_dict[construct + '_dqdKN'] \
         + KR/(q_dict[construct + '_q']) * dqdKR_dict[construct + '_dqdKR'] \
         + KX/(q_dict[construct + '_q']) * dqdKX_dict[construct + '_dqdKX'] \
         + KC/(q_dict[construct + '_q']) * dqdKC_dict[construct + '_dqdKC']) \
        / (len(pairs_list))

# The loop below replaces K's and W's the fraction folded terms in the
# dictionary with DGs, ms, and denaturant concentrations.  The simplify line
# is really important for making compact expressions for fraction folded.
# This simplification greatly speeds up fitting.  The last line
# converts from a sympy object to a string, to allow for json dump.

for construct in frac_folded_dict:
    frac_folded_dict[construct] = frac_folded_dict[construct].subs({
    KN:(exp(-((dGN + (mR*denat))/RT))),
    KR:(exp(-((dGR + (mR*denat))/RT))),
    KX:(exp(-((dGX + (mX*denat))/RT))),
    KC:(exp(-((dGC + (mR*denat))/RT))),
    TRR:(exp(-dGRR/RT)),
    TXX:(exp(-dGXX/RT)),
    TXR:(exp(-dGXR/RT)),
    TRX:(exp(-dGRX/RT)) })
    frac_folded_dict[construct] = sp.simplify(frac_folded_dict[construct])
    frac_folded_dict[construct] = str(frac_folded_dict[construct])

with open(os.path.join(path, f'{proj_name}_frac_folded_dict.json'), 'w') as f:
    json.dump(frac_folded_dict, f)

stop = time.time()
runtime = stop - start
print('\nThe elapsed time was ' + str(runtime) + ' sec')
```

## Calculate the rank of the coefficient matrix

The construct list is used to build a matrix of coefficeints for each energy term.  The user must input a list containing the thermodynamic parameters. In principle, these could be extracted from the SymPy symbols above, or maybe from the initial guesses list below, but that would require a fit to be done first, which is probably not a good idea for models that have incomplete rank.

```python
with open(os.path.join(path, f"{proj_name}_constructs.json"), 'r') as cons:
    constructs = json.load(cons)

num_constructs = len(constructs)
thermo_param_list = ['dGN','dGR','dGX','dGC','dGRR','dGXX','dGXR','dGRX']
num_params = len(thermo_param_list)

coeff_matrix = np.zeros((num_constructs, num_params))

row = 0
for construct in constructs:
    repeats_list = construct.split('_')
    # For first repeat, don't need (or want) to worry about interface.
    i = 0
    if repeats_list[i] == 'N':
        coeff_matrix[row, 0] = 1
    elif repeats_list[i] == 'R':
        coeff_matrix[row, 1] = coeff_matrix[row, 1] + 1
    elif repeats_list[i] == 'X':
        coeff_matrix[row, 2] = coeff_matrix[row, 2] + 1
    else:
        coeff_matrix[row, 3] = 1

    # For remaining repeats, need to worry about interfaces
    i = 1
    while i < len(repeats_list):
        if repeats_list[i] == 'R':
            coeff_matrix[row, 1] = coeff_matrix[row, 1] + 1
        elif repeats_list[i] == 'X':
            coeff_matrix[row, 2] = coeff_matrix[row, 2] + 1
        else:
            coeff_matrix[row, 3] = 1

        if repeats_list[i] == 'R':
            if (repeats_list[i-1] == 'N' or repeats_list[i-1] == 'R'):
                coeff_matrix[row, 4] = coeff_matrix[row, 4] + 1  # RR-type interface (includes NR)
            else:
                coeff_matrix[row, 6] = coeff_matrix[row, 6] + 1  # XR interface

        if repeats_list[i] == 'X':
            if (repeats_list[i-1] == 'N' or repeats_list[i-1] == 'R'):
                coeff_matrix[row, 7] = coeff_matrix[row, 7] + 1  # RX-type interface (includes NX)
            else:
                coeff_matrix[row, 5] = coeff_matrix[row, 5] + 1  # XX interface

        if repeats_list[i] == 'C':
            if (repeats_list[i-1] == 'R'):
                coeff_matrix[row, 4] = coeff_matrix[row, 4] + 1  # RR-type interface (RC)
            else:
                coeff_matrix[row, 6] = coeff_matrix[row, 6] + 1  # XR-type interface (XC)

        i =i + 1
    row = row + 1

rank = np.linalg.matrix_rank(coeff_matrix)

if rank == num_params:
    print("\nThe coefficeint matrix has full column rank (r=",rank,")\n") #leaves a space betw rank and ).  Not sure why.
else:
    print("\nThe coefficeint matrix has incomplete column rank (r=",rank,").")
    print("You should revise your model or include the necessary constructs to obtain full rank.\n")
```

## Fitting the data with the Ising model

Processed data files are imported along with the fraction-folded dictionary and construct and melt lists.  The fit is performed with the lmfit module, which has extra functionality over fitting routines in scipy.

Note that if your initial guesses are poor, the fit may be slowed significantly or the fit may not converge.

Fitted thermodynamic parameters are outputted to the screen and are written to a csv file.  Baseline parameters are also written to a csv file.

```python
print("\nFitting the data...\n")

start = time.time()

plt.close()
plt.clf

RT = 0.001987 * 298.15 #  R in kcal/mol/K, T in Kelvin.

#  Dictionary of frac folded eqns from partition function generator script.
with open(os.path.join(path, f'{proj_name}_frac_folded_dict.json'), 'r') as ffd:
    frac_folded_dict = json.load(ffd)

with open(os.path.join(path, f'{proj_name}_constructs.json'), 'r') as construct:
    constructs = json.load(construct)

with open(os.path.join(path, f'{proj_name}_melts.json'), 'r') as m:
    melts = json.load(m)

num_melts = len(melts)
num_constructs = len(constructs)

melt_data_dict = {}
for melt in melts:
   melt_data_dict[melt] = np.load(os.path.join(path, f'{melt}.npy'), allow_pickle=True)

# Compile fraction folded expressions.
comp_frac_folded_dict = {}
for construct in constructs:
    frac_folded_string = frac_folded_dict[construct + '_frac_folded']
    comp_frac_folded = compile(frac_folded_string, '{}_comp_ff'.format(construct), 'eval')
    comp_frac_folded_dict[construct + '_comp_ff'] = comp_frac_folded

# CREATE INITIAL GUESSES
# First, thermodynamic parameters.  These are Global.
init_guesses = lmfit.Parameters()
init_guesses.add('dGN', value = 5)
init_guesses.add('dGR', value = 4)
init_guesses.add('dGX', value = 5)
init_guesses.add('dGC', value = 6)
init_guesses.add('dGRR', value = -11)
init_guesses.add('dGXX', value = -10)
init_guesses.add('dGRX', value = -10)
init_guesses.add('dGXR', value = -10)
init_guesses.add('mR', value = 0.8)
init_guesses.add('mX', value = 0.8)

# Next, baseline parameters.  These are local.
for melt in melts:
    init_guesses.add('af_{}'.format(melt), value=0.02)
    init_guesses.add('bf_{}'.format(melt), value=1)
    init_guesses.add('au_{}'.format(melt), value=0.0)
    init_guesses.add('bu_{}'.format(melt), value=0.0)

# Transfers init_guesses to params for fitting, but init_guesses are maintained.
params = init_guesses

def fitting_function(params, denat, frac_folded, melt):
    af = params['af_{}'.format(melt)].value
    bf = params['bf_{}'.format(melt)].value
    au = params['au_{}'.format(melt)].value
    bu = params['bu_{}'.format(melt)].value
    dGN = params['dGN'].value
    dGR = params['dGR'].value
    dGX = params['dGX'].value
    dGC = params['dGC'].value
    dGRR = params['dGRR'].value
    dGXX = params['dGXX'].value
    dGRX = params['dGRX'].value
    dGXR = params['dGXR'].value
    mR = params['mR'].value
    mX = params['mX'].value
    return ((af * denat) + bf) * frac_folded + (((au * denat) + bu) * (1 - frac_folded))

# Objective function creates an array of residuals to be used by lmfit minimize.
def objective(params):
    resid_dict = {}
    dGN = params['dGN'].value
    dGR = params['dGR'].value
    dGC = params['dGC'].value
    dGX = params['dGX'].value
    dGRR = params['dGRR'].value
    dGXX = params['dGXX'].value
    dGRX = params['dGRX'].value
    dGXR = params['dGXR'].value
    mR = params['mR'].value
    mX = params['mX'].value
    for melt in melts:
        denat = melt_data_dict[melt][:,0]     # A numpy array of type str
        norm_sig = melt_data_dict[melt][:,1]  # A numpy array of type str
        denat = denat.astype(float) # A numpy array of type float
        norm_sig = norm_sig.astype(float) # A numpy array of type float
        string_to_eval = comp_frac_folded_dict[melt[:-2] + '_comp_ff']
        frac_folded = eval(string_to_eval)
        af = params['af_{}'.format(melt)].value
        bf = params['bf_{}'.format(melt)].value
        au = params['au_{}'.format(melt)].value
        bu = params['bu_{}'.format(melt)].value
        resid = norm_sig - fitting_function(params, denat, frac_folded, melt)
        resid_dict[melt + '_resid'] = resid
    residuals = np.concatenate(list(resid_dict.values()))
    return residuals

# Fit with lmfit
result = lmfit.minimize(objective, init_guesses)
fit_resid = result.residual

# Print out features of the data, the fit, and optimized param values
print("There are a total of {} data sets.".format(num_melts))
print("There are {} observations.".format(result.ndata))
print("There are {} fitted parameters.".format(result.nvarys))
print("There are {} degrees of freedom. \n".format(result.nfree))
print("The sum of squared residuals (SSR) is: {0:7.4f}".format(result.chisqr))
print("The reduced SSR (SSR/DOF): {0:8.6f} \n".format(result.redchi))

dGN = result.params['dGN'].value
dGR = result.params['dGR'].value
dGX = result.params['dGX'].value
dGC = result.params['dGC'].value
dGRR = result.params['dGRR'].value
dGXX = result.params['dGXX'].value
dGRX = result.params['dGRX'].value
dGXR = result.params['dGXR'].value
mR = result.params['mR'].value
mX = result.params['mX'].value

print('Optimized parameter values:')
print('dGN = {0:8.4f}'.format(result.params['dGN'].value))
print('dGR = {0:8.4f}'.format(result.params['dGR'].value))
print('dGX ={0:8.4f}'.format(result.params['dGX'].value))
print('dGC ={0:8.4f}'.format(result.params['dGC'].value))
print('dGRR ={0:8.4f}'.format(result.params['dGRR'].value))
print('dGXX ={0:8.4f}'.format(result.params['dGXX'].value))
print('dGRX ={0:8.4f}'.format(result.params['dGRX'].value))
print('dGXR ={0:8.4f}'.format(result.params['dGXR'].value))
print('mR ={0:8.4f}'.format(result.params['mR'].value))
print('mX ={0:8.4f}'.format(result.params['mX'].value))

print("\nWriting best fit parameter and baseline files")

# Compile a list of optimized Ising params and write to file.
fitted_ising_params = [["dGN", result.params['dGN'].value],
                        ["dGR", result.params['dGR'].value],
                        ["dGX", result.params['dGX'].value],
                        ["dGC", result.params['dGC'].value],
                        ["dGRR", result.params['dGRR'].value],
                        ["dGXX", result.params['dGXX'].value],
                        ["dGRX", result.params['dGRX'].value],
                        ["dGXR", result.params['dGXR'].value],
                        ["mR", result.params['mR'].value],
                        ["mX", result.params['mX'].value],
                        ["Chi**2",result.chisqr],
                        ["RedChi",result.redchi]]

with open(os.path.join(path, f'{proj_name}_fitted_Ising_params.csv'), "w") as n:
    writer = csv.writer(n, delimiter=',')
    writer.writerows(fitted_ising_params)
n.close()

# Compile a list of optimized baseline params and write to file.
fitted_base_params = []
for melt in melts:
    af = result.params['af_%s' % (melt)].value
    bf = result.params['bf_%s' % (melt)].value
    au = result.params['au_%s' % (melt)].value
    bu = result.params['bu_%s' % (melt)].value
    fitted_base_params.append([melt, af, bf, au, bu])
with open(os.path.join(path, f'{proj_name}_fitted_baseline_params.csv'), "w") as m:
    writer = csv.writer(m, delimiter=',')
    writer.writerows(fitted_base_params)
m.close()

stop = time.time()
runtime = stop - start
print('\nThe elapsed time was ' + str(runtime) + ' sec')
```

## Plotting the results of the fit

This cell generates four plots.  Two are "normalized" data (the data that were actually fit in the scipt above) and fits.  The other two are fraction-folded data and fits.  One each shows all the constructs, which ideally includes multiple melts of each construct, allowing all fits to be inspected.  The other shows only a single melt for each construct (the first one in the melt list for each), simplifying the plot.

The resulting plots are dumped to the screen below the cell, and are saved as png files.

Note that this script is meant to be run after the fitting script.  If the fit has not been performed in the current session (or the kernel was restarted after the fit--*not usually the case*), then imports will have to be run, along with data and fitted parameters.  That would be pain, so just re-run the fit again, if you find yourself in this situation.

```python
print("\nPlotting results...\n")

# The function "baseline_adj" gives an adjusted y value based on fitted baseline
# parameters (fraction folded).
def baseline_adj(y, x, params, construct):
    af = result.params['af_{}'.format(construct)].value
    bf = result.params['bf_{}'.format(construct)].value
    au = result.params['au_{}'.format(construct)].value
    bu = result.params['bu_{}'.format(construct)].value
    return (y-(bu+(au*x)))/((bf+(af*x))-(bu+(au*x)))

# Defining global best-fit parameters
dGN = result.params['dGN'].value
dGR = result.params['dGR'].value
dGX = result.params['dGX'].value
dGC = result.params['dGC'].value
dGRR = result.params['dGRR'].value
dGXX = result.params['dGXX'].value
dGRX = result.params['dGRX'].value
dGXR = result.params['dGXR'].value
mR = result.params['mR'].value
mX = result.params['mX'].value

# The function fit_model used for plotting best-fit lines and for adding
# residuals to best-fit lines in bootstrapping.  Normalized, not frac folded.
def fit_model(params, x, melt):
    denat = x
    af = result.params['af_{}'.format(melt)].value
    bf = result.params['bf_{}'.format(melt)].value
    au = result.params['au_{}'.format(melt)].value
    bu = result.params['bu_{}'.format(melt)].value
    frac_folded = eval(comp_frac_folded_dict[melt[:-2] + '_comp_ff']) # :-2 leaves off the _1, _2, etc from melt id.
    return ((af * denat) + bf) * frac_folded + (((au * denat) + bu) * \
            (1 - frac_folded))

# Finding the maximum denaturant value out of all the melts to
# set x axis bound
denat_maxer = np.zeros(0)
for melt in melts:
    denat_maxer = np.concatenate((denat_maxer, melt_data_dict[melt][:, 0]))
denat_maxer_list = denat_maxer.astype(float)
denat_max = np.max(denat_maxer_list)
denat_bound = np.around(denat_max, 1) + 0.2

# Denaturant values to use when evaluating fits.  Determines how smooth the
# fitted curve will be, based on the third value (300) in the argument below.
# I might keep using this for fraction_foldeed, but for nomralized baseline
# use a local set of points for each melt, so as not to extrapolate the
# bselines too far.
denat_fit = np.linspace(0, denat_bound, 300)

#defining a dictionary using the first melt of each construct (construct_1)
#Move this to the plotting part, and why not do this for all constructs?
construct1_data_dict = {}
for construct in constructs:
    construct1_data_dict[construct] = np.load(os.path.join(path, f'{construct}_1.npy'))

# The four dictionaries below define lower and upper denaturant limnits to be
# used for plotting normalized curves, so crazy-long baseline extrapolations
# are not shown.  Do both for melts and construct 1.   These are then used
# to create 300-point synthetic baselines in the fifth and sixth dictionaries.
melt_lower_denat_dict = {melt: round(np.min(melt_data_dict[melt][:,0].astype(float))) -0.2 for melt in melts}
melt_upper_denat_dict = {melt: round(np.max(melt_data_dict[melt][:,0].astype(float))) + 0.2 for melt in melts}

construct1_lower_denat_dict = {construct: round(np.min(construct1_data_dict[construct][:,0].astype(float))) - 0.2 for construct in constructs}

construct1_upper_denat_dict = {construct: round(np.max(construct1_data_dict[construct][:,0].astype(float))) + 0.2 for construct in constructs}

melt_denat_synthetic_dict = {
    melt: np.linspace(
        melt_lower_denat_dict[melt],
        melt_upper_denat_dict[melt],
        300
    )
    for melt in melts
}

construct1_denat_synthetic_dict = {
    construct: np.linspace(
        construct1_lower_denat_dict[construct],
        construct1_upper_denat_dict[construct],
        300
    )
    for construct in constructs
}

''' Global Plot Aesthetics'''
# Defining how the plots are colored
num_melt_colors = num_melts
num_construct_colors = num_constructs
coloration = plt.get_cmap('hsv')

# Dictonary defining title font
title_font = {'family': 'arial',
        'color': 'black',
        'weight': 'normal',
        'size': 16}

# Dictionary defining label font
label_font = {'family': 'arial',
        'color': 'black',
        'weight': 'normal',
        'size': 14}

'''First Plot: Fraction Folded by Melt'''
#extracting the melt data and creating plot lines for each melt
colorset = 0 # counter to control color of curves and points
for melt in melts:
    colorset = colorset + 1
    denat = melt_data_dict[melt][:,0]     # A numpy array of type str
    norm_sig = melt_data_dict[melt][:,1]  # A numpy array of type str
    denat = denat.astype(float) # A numpy array of type float
    norm_sig = norm_sig.astype(float) # A numpy array of type float
    y_adj = baseline_adj(norm_sig, denat, result.params, melt)
    y_fit = fit_model(result.params, denat_fit, melt)
    y_fit_adj = baseline_adj(y_fit, denat_fit, result.params, melt)
    plt.plot(denat, y_adj, 'o', color = coloration(colorset/num_melt_colors),
            label = melt[:-2] + ' melt ' + melt[-1])
    plt.plot(denat_fit, y_fit_adj, '-', color = coloration(colorset/num_melt_colors))

#set axis limits
axes=plt.gca()
axes.set_xlim([-0.1, denat_bound])
axes.set_ylim([-0.1,1.1])
axes.set_aspect(5.5)

#lot aesthetics and labels
plt.legend(loc = 'center', bbox_to_anchor = (1.25, 0.5), fontsize=8)
plt.title('Fraction Folded by Melt', fontdict = title_font)
plt.xlabel('Denaturant (Molar)', fontdict = label_font)
plt.ylabel('Fraction Folded', fontdict = label_font)

#saving plot in individual doc
plt.savefig(os.path.join(path, f'{proj_name}_plot_frac_folded_by_melt.png'),\
            dpi = 500, bbox_inches='tight')

#show plot in iPython window and then close
plt.show()
plt.close()
plt.clf

'''Second Plot: Normalized Signal by Melt'''
colorset = 0
for melt in melts:
    colorset = colorset + 1
    denat = melt_data_dict[melt][:,0]     # A numpy array of type str
    norm_sig = melt_data_dict[melt][:,1]  # A numpy array of type str
    denat = denat.astype(float) # A numpy array of type float
    norm_sig = norm_sig.astype(float) # A numpy array of type float
    y_fit = fit_model(result.params, melt_denat_synthetic_dict[melt], melt)
    plt.plot(denat, norm_sig, 'o', color=coloration(colorset/num_melt_colors),
             label = melt[:-2] + ' melt ' + melt[-1])
    plt.plot(melt_denat_synthetic_dict[melt], y_fit, '-', \
             color=coloration(colorset/num_melt_colors))

#set axis limits
axes=plt.gca()
axes.set_xlim([-0.1, denat_bound])
axes.set_ylim([-0.1,1.1])
axes.set_aspect(5.5)

#plot aesthetics and labels
plt.legend(loc = 'center', bbox_to_anchor = (1.25, 0.5), fontsize=8)
plt.title('Normalized Signal by Melt', fontdict = title_font)
plt.xlabel('Denaturant (Molar)', fontdict = label_font)
plt.ylabel('Normalized Signal', fontdict = label_font)

#saving plot in individual doc
plt.savefig(os.path.join(path, f'{proj_name}_plot_normalized_by_melt.png'),\
            dpi=500, bbox_inches='tight')

#show plot in iPython window and then close
plt.show()
plt.close()
plt.clf

'''Third Plot: Fraction Folded by Construct'''
colorset = 0
for construct in constructs:
    colorset = colorset + 1
    denat = construct1_data_dict[construct][:,0]  # A numpy array of type str
    denat_line = construct1_data_dict[construct][:, 0]   # A numpy array of type str
    norm_sig = construct1_data_dict[construct][:, 1]  # A numpy array of type str
    denat = denat.astype(float) # A numpy array of type float
    denat_line = denat_line.astype(float) # A numpy array of type float
    norm_sig = norm_sig.astype(float) # A numpy array of type float
    y_adj = baseline_adj(norm_sig, denat_line, result.params, construct + '_1')
    y_fit = fit_model(result.params, denat_fit, construct + '_1')
    y_fit_adj = baseline_adj(y_fit, denat_fit, result.params, construct + '_1')
    plt.plot(denat, y_adj, 'o', \
             color=coloration(colorset/num_construct_colors), label = construct)
    plt.plot(denat_fit, y_fit_adj, '-', \
             color=coloration(colorset/num_construct_colors))

#set axis limits
axes=plt.gca()
axes.set_xlim([-0.1, denat_bound])
axes.set_ylim([-0.1,1.1])
axes.set_aspect(5.5)

#plot aesthetics and labels
plt.legend(loc = 'center', bbox_to_anchor = (1.15, 0.5), fontsize=8)
plt.title('Fraction Folded by Construct', fontdict = title_font)
plt.xlabel('Denaturant (Molar)', fontdict = label_font)
plt.ylabel('Fraction Folded', fontdict = label_font)

#saving plot in individual doc
plt.savefig(os.path.join(path, f'{proj_name}_plot_frac_folded_by_construct.png'),\
            dpi=500, bbox_inches='tight')

#show plot in iPython window and then close
plt.show()
plt.close()
plt.clf

'''Fourth Plot: Normalized Signal by Construct'''
colorset = 0
for construct in constructs:
    colorset = colorset + 1
    denat = construct1_data_dict[construct][:,0]     # A numpy array of type str
    norm_sig = construct1_data_dict[construct][:,1]  # A numpy array of type str
    denat = denat.astype(float) # A numpy array of type float
    norm_sig = norm_sig.astype(float) # A numpy array of type float
    y_fit = fit_model(result.params, construct1_denat_synthetic_dict[construct], \
                      construct + '_1')
    plt.plot(denat, norm_sig, 'o', color = coloration(colorset/num_construct_colors),
             label = construct)
    plt.plot(construct1_denat_synthetic_dict[construct], y_fit, '-', \
             color = coloration(colorset/num_construct_colors))

#set axis limits
axes=plt.gca()
axes.set_xlim([-0.1, denat_bound])
axes.set_ylim([-0.1,1.1])
axes.set_aspect(5.5)

#plot aesthetics and labels   * ``````
plt.legend(loc = 'center', bbox_to_anchor = (1.15, 0.5), fontsize=8)
plt.title('Normalized Signal by Construct', fontdict = title_font)
plt.xlabel('Denaturant (Molar)', fontdict = label_font)
plt.ylabel('Normalized Signal', fontdict = label_font)

#saving plot in individual doc
plt.savefig(os.path.join(path, f'{proj_name}_plot_normalized_by_construct.png'),\
            dpi=500, bbox_inches='tight')

#show plot in iPython window and then close
plt.show()
plt.close()
plt.clf
```

## Bootstrap analysis

Asks the user to input the number of bootstrap iterations.  Bootstrap parameters are stored in a list of lists (**bs_param_values**). After performing the specified number of iterations, bootstrapped thermodynamic parameters are written to a csv file.

Again, bootstrapping is meant to be performed after fitting above.  Otherwise, the data and the fit model will have to be re-imported, and the params list and objective function will need to be generated.  Just run the fit again if needed.

In this version, a two minute sleep command is built in every 50 bootstrap iterations to let things cool down.

```python
'''BootStrap analysis'''
# Create list to store bootstrap iterations of values and define column titles
bs_param_values = []
bs_param_values.append(['Bootstrap Iter', 'dGN', 'dGR', 'dGX', 'dGC', 'dGRR',
                        'dGXX', 'dGRX', 'dGXR', 'mR', 'mX', 'redchi**2','bestchi**2'])
#total number of bootstrap iterations
bs_iter_tot = input("How many bootstrap iterations? ")

#bs_iter_tot = 10  # You would use this if you did not want user input from screen
bs_iter_count = 0   # Iteration counter
fit_resid_index= len(fit_resid) - 1

y_fitted_dict = {}
# Dictionary of 'true' normalized y values from fit at each denaturant value.
for melt in melts:
    denat = melt_data_dict[melt][:,0] # A numpy array of type str
    denat = denat.astype(float)       # A numpy array of type float
    y_fitted_dict[melt] = np.array(fit_model(result.params, denat, melt))

# Arrays to store bs fitted param values
dGN_vals = []
dGR_vals = []
dGX_vals = []
dGC_vals = []
dGRR_vals = []
dGXX_vals = []
dGRX_vals = []
dGXR_vals = []
mR_vals = []
mX_vals = []

# Add residuals chosen at random (with replacement) to expected
# y values. Note-residuals are combined ACROSS melts.
for j in range(int(bs_iter_tot)):
    rand_resid_dict={}  # Clears the random data for each bootsterap iteration
    bs_iter_count = bs_iter_count + 1
    print("Bootstrap iteration {0} out of {1}".format(bs_iter_count,
                               bs_iter_tot))

    for melt in melts:
        rand_resid =[]
        denat = melt_data_dict[melt][:,0] # A numpy array of type str
        denat = denat.astype(float)       # A numpy array of type float

        for x in range(len(denat)):  # Creastes a list of random residuals
            rand_int = np.random.randint(0, fit_resid_index)
            rand_resid.append(fit_resid[rand_int])

        rand_resid_dict[melt] = np.array(rand_resid)
        y_bootstrap = y_fitted_dict[melt] + rand_resid_dict[melt]
        z_max,z_min = y_bootstrap.max(), y_bootstrap.min()
        melt_data_dict[melt][:, 1] = (y_bootstrap - z_min)/(z_max - z_min)

    bs_result = lmfit.minimize(objective, init_guesses)
    bs_chisqr = bs_result.chisqr
    bs_red_chisqr= bs_result.redchi

    dGN = bs_result.params['dGN'].value
    dGR = bs_result.params['dGR'].value
    dGX = bs_result.params['dGX'].value
    dGC = bs_result.params['dGC'].value
    dGRR = bs_result.params['dGRR'].value
    dGXX = bs_result.params['dGXX'].value
    dGRX = bs_result.params['dGRX'].value
    dGXR = bs_result.params['dGXR'].value
    mR = bs_result.params['mR'].value
    mX = bs_result.params['mX'].value

    # Store each value in a list for plotting and for downstream statistical analysis
    dGN_vals.append(dGN)
    dGR_vals.append(dGR)
    dGX_vals.append(dGX)
    dGC_vals.append(dGC)
    dGRR_vals.append(dGRR)
    dGXX_vals.append(dGXX)
    dGRX_vals.append(dGRX)
    dGXR_vals.append(dGXR)
    mR_vals.append(mR)
    mX_vals.append(mX)

    # Append bootstrapped global parameter values for ouput to a file
    bs_param_values.append([bs_iter_count, dGN, dGR, dGX, dGC, dGRR, dGXX, dGRX, dGXR, mR, mX,
                            bs_red_chisqr,bs_chisqr])

with open(os.path.join(path, f'{proj_name}_bootstrap_params.csv'), "w") as n:
    writer = csv.writer(n, delimiter = ',')
    writer.writerows(bs_param_values)
n.close()
```

## The next cell calculates statistical properties of bootstrap parameters and outputs a file

I plan to merge this with the bootstrap cell, but it is much more convenient to code it separately.

The structure that currently holds the bootstrap parameter values (*bs_param_values*) is a list of lists.  So it needs to be converted to a numpy array, and it needs to have only values, not column heads, in order to do numerical calculations.  Pandas would clearly be the right way to go with this, but not today.

*path* (for writing out the data frame) is taken from the fitting cell above.

```python
bs_param_values_fullarray = np.array(bs_param_values)
bs_param_values_array = bs_param_values_fullarray[1:,1:-2].astype(np.float) # End at -2 since last two columns
                                                                            # are chi square statistics

bs_param_names = bs_param_values_fullarray[0][1:-2]

statistics = ['mean','median','stdev','2.5% CI','16.6% CI','83.7% CI','97.5% CI']

bs_statistics_df = pd.DataFrame(columns = statistics)

i = 0
for param in bs_param_names:
    bs_statistics = []
    bs_statistics.append(np.mean(bs_param_values_array[:,i]))
    bs_statistics.append(np.median(bs_param_values_array[:,i]))
    bs_statistics.append(np.std(bs_param_values_array[:,i]))
    bs_statistics.append(np.percentile(bs_param_values_array[:,i],2.5))
    bs_statistics.append(np.percentile(bs_param_values_array[:,i],16.7))
    bs_statistics.append(np.percentile(bs_param_values_array[:,i],83.3))
    bs_statistics.append(np.percentile(bs_param_values_array[:,i],97.5))
    bs_statistics_df.loc[param] = bs_statistics
    i = i + 1

bs_statistics_df.to_csv(os.path.join(path, f'{proj_name}_bootstrap_stats.csv'))

corr_coef_matrix = np.corrcoef(bs_param_values_array, rowvar = False)
corr_coef_df = pd.DataFrame(corr_coef_matrix, columns = bs_param_names, index = bs_param_names)
corr_coef_df.to_csv(os.path.join(path, f'{proj_name}_bootstrap_corr_coefs.csv'))
```

```python
bs_statistics_df
```

```python
corr_coef_matrix
```

```python
corr_coef_df
```

## Bootstrap histograms and correlation plots

Plots are generated for the thermodynamic parameters of interest (currently, baseline parameters are not included, thought this would not be hard to generate).  Histograms are generated for each parameter.  Scatter plots are generated for each pair of parameters (not including self-correlation) and arrayed in a grid along with a linear fit.  Shared axes are used in the grid to minimize white-space resulting from labelling each axis.  Thinking about the output as a matrix, the histograms are on the main diagonal, and the correllation plots are off-diagonal elements populating the upper triangle of the matrix.

The plot grid is dumped to the screen below, and is also written as a pdf file.

As with the plotting and bootstrapping scripts above, this is meant to be run after the fitting script (and after the bootstrapping script immediately above).  If you have not done that, re-run fit and bootstrap.

```python
# Specify the names of parameters to be compared to see correlation.
corr_params = ['dGN', 'dGR', 'dGX', 'dGC', 'dGRR', 'dGXX', 'dGRX', 'dGXR', 'mR', 'mX']

# These are a second set of parameter names that follow in the same order
# as in corr_params.  They are formatted using TeX-style names so that Deltas
# and subscripts will be plotted.  The would not be good key names for dictionaries
corr_param_labels = ['$\Delta$G$_N$', '$\Delta$G$_R$', '$\Delta$G$_X$', '$\Delta$G$_C$',
               '$\Delta$G$_{RR}$', '$\Delta$G$_{XX}$', '$\Delta$G$_{RX}$', '$\Delta$G$_{XR}$', 'm$_R$', 'm$_X$']

num_corr_params = len(corr_params)
gridsize = num_corr_params  # Determines the size of the plot grid.

# Dictionary of fitted parameter values.
corr_params_dict = {'dGN': dGN_vals, 'dGR': dGR_vals, 'dGX': dGX_vals, 'dGC': dGC_vals,\
                    'dGRR': dGRR_vals, 'dGXX': dGXX_vals, 'dGRX': dGRX_vals, 'dGXR': dGXR_vals,
                    'mR': mR_vals, 'mX': mX_vals}

# PDF that stores a grid of the correlation plots
with PdfPages(os.path.join(path, f'{proj_name}_Corr_Plots.pdf')) as pdf:
    fig, axs = plt.subplots(ncols=gridsize, nrows=gridsize, figsize=(20, 20))

    # Turns off axes on lower triangle
    axs[1, 0].axis('off')
    axs[2, 0].axis('off')
    axs[2, 1].axis('off')
    axs[3, 0].axis('off')
    axs[3, 1].axis('off')
    axs[3, 2].axis('off')
    axs[4, 0].axis('off')
    axs[4, 1].axis('off')
    axs[4, 2].axis('off')
    axs[4, 3].axis('off')
    axs[5, 0].axis('off')
    axs[5, 1].axis('off')
    axs[5, 2].axis('off')
    axs[5, 3].axis('off')
    axs[5, 4].axis('off')
    axs[6, 0].axis('off')
    axs[6, 1].axis('off')
    axs[6, 2].axis('off')
    axs[6, 3].axis('off')
    axs[6, 4].axis('off')
    axs[6, 5].axis('off')
    axs[7, 0].axis('off')
    axs[7, 1].axis('off')
    axs[7, 2].axis('off')
    axs[7, 3].axis('off')
    axs[7, 4].axis('off')
    axs[7, 5].axis('off')
    axs[7, 6].axis('off')
    axs[8, 0].axis('off')
    axs[8, 1].axis('off')
    axs[8, 2].axis('off')
    axs[8, 3].axis('off')
    axs[8, 4].axis('off')
    axs[8, 5].axis('off')
    axs[8, 6].axis('off')
    axs[8, 7].axis('off')
    axs[9, 0].axis('off')
    axs[9, 1].axis('off')
    axs[9, 2].axis('off')
    axs[9, 3].axis('off')
    axs[9, 4].axis('off')
    axs[9, 5].axis('off')
    axs[9, 6].axis('off')
    axs[9, 7].axis('off')
    axs[9, 8].axis('off')

    # Defines the position of the y paramater from the array of params
    hist_param_counter = 0
    while hist_param_counter < num_corr_params:
        hist_param_label = corr_param_labels[hist_param_counter]
        hist_param = corr_params[hist_param_counter]
        # Start fixing labels here
        #plt.xticks(fontsize=8)
        #axs[hist_param_counter, hist_param_counter].tick_params(fontsize=8)
        #axs[hist_param_counter, hist_param_counter].yticks(fontsize=8)
        axs[hist_param_counter, hist_param_counter].hist(corr_params_dict[hist_param])
        axs[hist_param_counter, hist_param_counter].set_xlabel(hist_param_label,
                   fontsize=14, labelpad = 5)
        hist_param_counter = hist_param_counter + 1

    # This part generates the correlation plots
    y_param_counter = 0
    while y_param_counter < num_corr_params - 1:
        # Pulls the parameter name for the y-axis label (with TeX formatting)
        yparam_label = corr_param_labels[y_param_counter]
        # Pulls the parameter name to be plotted on the y-axis
        yparam = corr_params[y_param_counter]

        # Defines the position of the x paramater from the array of params.
        # The + 1 offest avoids correlating a parameter with itself.
        x_param_counter = y_param_counter + 1

        while (x_param_counter < num_corr_params):
            #pulls the parameter name for the x-axis label (with TeX formatting)
            xparam_label = corr_param_labels[x_param_counter]
            # Pulls the parameter name to be plotted on the x-axis
            xparam = corr_params[x_param_counter]

            x_vals= corr_params_dict[xparam]
            y_vals = corr_params_dict[yparam]

            #plt.xticks(fontsize=8)
            #plt.yticks(fontsize=8)
            #plotting scatters with axes.  +1 shifts a plot to the right from main diagonal
            axs[y_param_counter, x_param_counter].plot(x_vals, y_vals, '.')

            # The if statement below turns off numbers on axes if not the right column and
            # not the main diagonal.
            if x_param_counter < num_corr_params - 1:
                axs[y_param_counter, x_param_counter].set_xticklabels([])
                axs[y_param_counter, x_param_counter].set_yticklabels([])

            if y_param_counter == 0:  # Puts labels above axes on top row
                axs[y_param_counter, x_param_counter].xaxis.set_label_position('top')
                axs[y_param_counter, x_param_counter].set_xlabel(xparam_label,
                   labelpad = 10, fontsize=14)
                axs[y_param_counter, x_param_counter].xaxis.tick_top()
                if x_param_counter < num_corr_params - 1:  # Avoids eliminating y-scale from upper right corner
                    axs[y_param_counter, x_param_counter].set_yticklabels([])

            if x_param_counter == num_corr_params - 1:  #  Puts labels right of right column
                axs[y_param_counter, x_param_counter].yaxis.set_label_position('right')
                axs[y_param_counter, x_param_counter].set_ylabel(yparam_label,
                   rotation = 0, labelpad = 30, fontsize=14)
                axs[y_param_counter, x_param_counter].set_xticklabels([])
                axs[y_param_counter, x_param_counter].yaxis.tick_right()

            # Determin correlation coefficient and display under subplot title
            # Note, there is no code that displays this value at the moment.
            #corr_coef = np.around(np.corrcoef(x_vals, y_vals), 3)

            #min and max values of the x param
            x_min = np.min(x_vals)
            x_max = np.max(x_vals)

            #fitting a straight line to the correlation scatterplot
            fit_array = np.polyfit(x_vals, y_vals, 1)
            fit_deg1_coef = fit_array[0]
            fit_deg0_coef = fit_array[1]
            fit_x_vals = np.linspace(x_min, x_max, 10)
            fit_y_vals = fit_deg1_coef*fit_x_vals + fit_deg0_coef

            #plotting correlation line fits
            axs[y_param_counter, x_param_counter].plot(fit_x_vals,
               fit_y_vals)
            plt.subplots_adjust(wspace=0, hspace=0)

            x_param_counter = x_param_counter + 1
        y_param_counter = y_param_counter + 1

    pdf.savefig(bbox_inches='tight')
```


## 1. Background

The Variantional Bayesian (VB) approach is to optimize an approximation to the intractable posterior. However, the common mean-field approach requires analytical solutions of expectation w.r.t. the approximate posterior which is also intractable in many cases. In this paper, we introduce the Stochastic Gradient Variantional Bayes (SGVB) estimator which is used for an efficient approximation of the posterior distribution in almost any case with continuous latent variables. The main idea is to yield a simple differentiable unbiased estimator of the variational lower bound based on a reparameterizatio of the lower bound. The optimization involved in this approach uses standard stochastic gradient ascent techniques. Furthermore, we propose the Auto-Encoding VB (AEVB) algorithm to implement this approach. The SGVB estimator is used in the AEVB algorithm to optimize a recognition model that allows us to efficiently approximate the posterior inference with simple ancestral sampling, which results in an efficient learning of the model parameters without costly iterative inference schemes (such as MCMC) per datapoint. Especially, it is called the variational auto-encoder when we use a neural network for the recognition model.

## 2. Algorithm

### 2.1 Problem Setting

1. The dataset $X = \{x^{(i)}\}^N_{i=1}$ consists of $N$ i.i.d samples of some continuous or discrete variable $x$. They are generated from some random process with an unobserved continuous random variable $z$.
2. $z^{(i)}$ is generated from some prior distribution $p_{\theta^*}(z)$.
3. $x^{(i)}$ is generated from some conditional distribution $p_{\theta^*}(x|z)$.
4. Both $p_{\theta^*}(z)$ and $p_{\theta^*}(x|z)$ are from parametric families of distributions $p_\theta(z)$ and $p_\theta(x|z)$, and their PDFs are differentiable almost everywhere w.r.t. both $\theta$ and $z$. However, we do not make the common simplifying assumptions about the marginal or posterior probabilities.
5. $q_\phi(z|x)$ is the recognition model, an approximation of the intractable true posterior $p_\theta(z|x)$. It is not necessarily factorial and its parameter $\phi$ are not computed from some closed-form expectation.
6. In the aspect of coding theory, the unobserved $z$ is interprated as a code, $q_\phi(z|x)$ is a probabilistic encoder and $p_\theta(x|z)$ is a probabilistic decoder.

### 2.2 Method

The marginal likelihood $\log p_\theta(x^{(1)},...,x^{(N)})=\sum^N_{i=1}\log p_\theta(x^{(i)})$ can be rewritten as:

\begin{equation}
\log p_\theta(x^{(i)})=D_{KL}(q_\phi(z|x^{(i)})||p_\theta(z|x^{(i)}))+L(\theta, \phi; x^{(i)})
\end{equation}

where the first RHS term is the KL divergence of the approximate from the true posterior and it is non-negative. The second RHS term $L(\theta, \phi; x^{(i)})$ is the variational lower bound on the marginal likelihood of the datapoint $i$. Thus we have:

\begin{equation}
\log p_\theta(x^{(i)})\geq L(\theta, \phi; x^{(i)})=E_{q_\phi(z|x)}[-\log q_\phi(z|x)+\log p_\theta(x,z)]\\
=-D_{KL}(q_\phi(z|x^{(i)}))||p_\theta(z|x^{(i)}))+E_{q_\phi(z|x^{(i)})}[p_\theta(x^{(i)}|z)]
\end{equation}

The point is to differentiate and optimize the lower bound $L(\theta, \phi; x^{(i)})$ w.r.t both the variational parameters $\phi$ and generative parameters $\theta$.

With a chosen $q_\phi(z|x)$, we use a differentiable transformation $g_\phi(\epsilon, x)$ to reparameterize the random variable $\widetilde{z}\sim q_\phi(z|x)$, where $\epsilon$ is an auxiliary noise:

\begin{equation}
\widetilde{z}=g_\phi(\epsilon, x), \epsilon\sim p(\epsilon)
\end{equation}

Here we can form Monte Carlo estimates of expectations of some function $f(z)$ w.r.t $q_\phi(z|x)$ as follows:

\begin{equation}
E_{q_\phi(z|x^{(i)})}[f(z)]=E_{p(\epsilon)}[f(g_\phi (\epsilon, x^{(i)}))]\simeq\frac{1}{L}\sum^L_{l=1}f(g_\phi (\epsilon^{(l)}, x^{(i)})), \epsilon^{(l)}\sim p(\epsilon)
\end{equation}

We apply this technique to the variational lower bound and yield the generic SGVB estimator $\widetilde{L}^A(\theta, \phi; x^{(i)})\simeq L(\theta, \phi; x^{(i)})$:

\begin{equation}
\widetilde{L}^A(\theta, \phi; x^{(i)})=\frac{1}{L}\sum^L_{l=1}\log p_\theta (x^{(i)}, z^{(i, l)})-\log q_\phi(z^{(i, l)}|x^{(i)})\\
z^{(i,l)}=g_\phi (\epsilon^{(i, l)}, x^{(i)}), \epsilon^{(l)}\sim p(\epsilon)
\end{equation}

When $D_{KL}(q_\phi(z|x^{(i)})||p_\theta(z|x^{(i)}))$ can be analytically integrated, we only need to estimate the expected reconstruction error $E_{q_\phi(z|x^{(i)})}[p_\theta(x^{(i)}|z)]$ by sampling. Thus we have the second version of the SGVB estimator $\widetilde{L}^B(\theta, \phi; x^{(i)})\simeq L(\theta, \phi; x^{(i)})$:

\begin{equation}
\widetilde{L}^B(\theta, \phi; x^{(i)})=-D_{KL}(q_\phi(z|x^{(i)}))||p_\theta(z|x^{(i)}))+\frac{1}{L}\sum^L_{l=1}\log p_\theta (x^{(i)}|z^{(i, l)})\\
z^{(i,l)}=g_\phi (\epsilon^{(i, l)}, x^{(i)}), \epsilon^{(l)}\sim p(\epsilon)
\end{equation}

Actually, the KL-divergence term can be interpreted as regularizing $\phi$ and $\widetilde{L}^B(\theta, \phi; x^{(i)})$ typically has less variance than $\widetilde{L}^A(\theta, \phi; x^{(i)})$.

### 2.3 AEVB Algorithm

First, we recall the autoencoder architecture:
$$\phi: X\to F \\
\psi: F\to X \\
\phi, \psi = \arg\min||X-(\psi\circ\phi) X||^2$$

In the simplest case, where there is one hidden layer, the encoder stage of an autoencoder takes the input $x\in \mathbb{R}^d = X$ that is then mapped to the latent code $z\in\mathbb{R}^p = F$:

\begin{equation}
\mathbf{z}=\sigma(\mathbf{Wx}+\mathbf{b})
\end{equation}

where $\sigma$ is an element-wise activation function such as a sigmoid function or a rectified linear unit. $\mathbf {W}$ s a weight matrix and $\mathbf {b}$ is a bias vector. After that, the decoder stage of the autoencoder maps $\mathbf {z}$ to the reconstruction $\mathbf {x'}$ of the same shape as $\mathbf{x}$:

\begin{equation}
\mathbf{z'}=\sigma'(\mathbf{W'x'}+\mathbf{b'})
\end{equation}

where $\mathbf {\sigma '} ,\mathbf {W'}$ and $\mathbf {b'}$ for the decoder may differ in general from the corresponding $\mathbf {\sigma } ,\mathbf {W}$ and $\mathbf {b}$ for the encoder.

Autoencoders are also trained to minimise reconstruction errors:

\begin{equation}
\mathbf{L(x, x')} = ||\mathbf{x}-\mathbf{x'}||^2 = ||\mathbf{x}-\sigma'(\mathbf{W'}(\sigma(\mathbf{Wx+b}))+\mathbf{b'})||^2
\end{equation}

Now it is clear to see the connection between SGVB estimator and auto-encoders above, which is that $\widetilde{L}^A(\theta, \phi; x^{(i)})$ or $\widetilde{L}^B(\theta, \phi; x^{(i)})$ should be the objective funtion ($\mathbf{L}$ above) for optimization. Therefore, we summarize the entire algorithm (a minibatch version) as follows:

- Initialize parameters $\mathbf{\theta_0, \phi_0}$

$\textbf{repeat}$:

1. $X^M$: Random minibatch of M datapoints (drawn from full dataset with the size of N)
2. $\mathbf{\epsilon}$: Random samples from noise distribution $p(\epsilon)$
3.
\fbox{Encoding: $X\to Z=g_\phi(X, \epsilon)$ and
Decoding: $Z\to \log p_\theta(X|Z)$}  $\to \widetilde{L}(\mathbf{\phi}, \mathbf{\theta}; \mathbf{X}, \mathbf{\epsilon})$ (either of the two SGVB estimators above can be used)
4. $\nabla_{\theta, \phi}\widetilde{L}^M(\mathbf{\phi}, \mathbf{\theta}; \mathbf{X^M}, \mathbf{\epsilon})$: gradient of the minibatch estimators, where $\widetilde{L}^M(\mathbf{\phi}, \mathbf{\theta}; \mathbf{X^M}, \mathbf{\epsilon})=\frac{N}{M}\sum_{i=1}^M\widetilde{L}(\mathbf{\phi}, \mathbf{\theta}; x^{(i)}, \mathbf{\epsilon})$
5. Update $\mathbf{\theta, \phi}$ using $\nabla_{\theta, \phi}\widetilde{L}^M(\mathbf{\phi}, \mathbf{\theta}; \mathbf{X}, \mathbf{\epsilon})$ (e.g. SGD)

$\textbf{until}$: convergence of parameters ($\mathbf{\theta, \phi}$)

$\textbf{return}$: $\mathbf{\theta, \phi}$

Thus, we find that parameters $\theta, \phi$ are jointly optimized via the AEVB algorithm.

## 3. Implementation

In terms of implementing the AEVB algorithm, we use a neural network for the probabilistic encoding and decoding parts. Let the prior over the latent variables be the centered isotropic multivariate Gaussian $p_\theta(\mathbf{z})=\mathcal{N}(\mathbf{z};\mathbf{0}, \mathbf{I})$. We assume the true (but intractable) posterior takes on a ap- proximate Gaussian form with an approximately diagonal covariance. In this case, we can let the variational approximate posterior be a multivariate Gaussian with a diagonal covariance structure:

$$\log q_\phi(\mathbf{z}|\mathbf{x}^{(i)})=\log\mathcal{N}(\mathbf{z}; \mathbf{\mu}^{(i)}, \mathbf{\sigma}^{2(i)}\mathbf{I})$$

where the mean and s.d. of the approximate posterior, $\mathbf{\mu}^{(i)}$ and $\mathbf{\sigma}^{2(i)}$, are outputs of the encoding MLP (a fully-connected neural network with a single hidden layer), i.e. nonlinear functions of datapoint $x^{(i)}$ and the variational parameters $\phi$:

\begin{equation}
\log q_\phi=\log\mathcal{N}(\mathbf{z}; \mathbf{\mu}, \mathbf{\sigma}^2\mathbf{I})\\
\mathbf{\mu}=\mathbf{W_1h}+\mathbf{b_1}\\
\log\mathbf{\sigma}^2=\mathbf{W_2h}+\mathbf{b_2}\\
\mathbf{h}=\tanh(\mathbf{W_3z}+\mathbf{b_3})
\end{equation}

Then, we sample from the posterior $\mathbf{z}^{(i,l)}\sim q_\phi(\mathbf{z}|\mathbf{x}^{(i)})$ using $\mathbf{z}^{(i,l)}=g_\phi(\mathbf{x}^{(i)},\epsilon^{(l)})=\mu^{(i)}+\sigma^{(i)}\bigodot\epsilon^{(l)}$ where $\epsilon^{(l)}\sim\mathcal{N}(\mathbf{0},\mathbf{I})$. With $\bigodot$ we signify an element-wise product. Since both $p_\theta(\mathbf{z})$ and $q_\phi(\mathbf{z}|\mathbf{x})$ are Gaussian, we use $\widetilde{L}^B$ with an analytically solved KL divergence. Therefore, we derive the resulting estimator as follows:

\begin{equation}
L(\theta, \phi; \mathbf{x}^{(i)})\simeq\frac{1}{2}\sum_{j=1}^J(1+\log((\sigma_j^{(i)})^2)-(\mu_j^{(i)})^2-(\sigma_j^{(i)})^2)+\frac{1}{L}\sum_{l=1}^{L}\log p_\theta(\mathbf{x}^{(i)}|\mathbf{z}^{(i,l)})
\end{equation}

Given the type of data that we are going to work on, we use the Gaussian MLP as decoder to achieve $\log p_\theta(\mathbf{x}^{(i)}|\mathbf{z}^{(i,l)})$:

\begin{equation}
\log p_\theta=\log\mathcal{N}(\mathbf{x}; \mathbf{\mu}', \mathbf{\sigma}'^2\mathbf{I})\\
\mathbf{\mu}'=\mathbf{W_4h'}+\mathbf{b_4}\\
\log\mathbf{\sigma}'^2=\mathbf{W_5h'}+\mathbf{b_5}\\
\mathbf{h'}=\tanh(\mathbf{W_6x}+\mathbf{b_6})
\end{equation}

$\{\mathbf{W}_i, \mathbf{b}_i\}_{i=1}^6$ are the weights and biases of the MLPs for encoder and decoder. To be clear, $\mathbf{\phi}=\{\mu, \sigma\}$ and $\mathbf{\theta}=\{\mu', \sigma'\}$ are parameters to be optimized.

Therefore, we summarize the iterative process of updating $\phi$ and $\theta$ as follows:

We offer some specific settings for the implementation on images from the MNIST and Frey Face datasets:

1. The size of minibatch $M=100$ and the size of samples per datapoint $L=1$.
2. The initialized values of parameters $\phi_0$ and $\theta_0$ are randomly sampled from $\mathcal{N}(0,0.01)$.
3. Stepsizes used for updating parameters within the algorithm are adapted by Adagrad with parameters chosen from $\{0.01, 0.02, 0.1\}$.
4. In terms of obtaining the likelihood lower bound, we trained generative models (decoders) and corresponding encoders (a.k.a. recognition models) having 500 hidden units in case of MNIST, and 200 hidden units in case of the Frey Face dataset.
5. In terms of estimating the marginal likelihood of the learned generative models with an MCMC estimator, we again used neural networks for the encoder and decoder, this time with 100 hidden units and 3 latent variables in case of MNIST dataset.

```python

```

```python

```


```python
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd

import wot
```

# Notebook 2: Computing transport matrices

In this notebook we compute transport matrices connecting each pair of time-points, and we examine the effect of each parameter on the solution.

To compute the transport matrix $\pi_{t_1,t_2}$ connecting cells $x_1, \ldots, x_n$ at time $t_1$ to cells $y_1, \ldots, y_m$ at time $t_2$, we solve an optimization problem over all matrices $\pi$ that obey certain row-sum and column-sum constraints.
These constraints ensure that the total amount of mass flowing out of each cell $x_i$ and into each cell $y_j$ adds up the correct amount.
We select the transport matrix with the lowest possible transport cost, subject to these constraints.

The amount of mass flowing out of each cell $x_i$ depends on the *growth rate* of the cell. We do this in step 1 below, before proceeding to computing transport maps in step 2.

# Step 1: Construct initial estimate of cell growth rates (optional)

Before computing transport maps, we first form an initial estimate of the cell growth rate function $g(x)$.
We do this using signatures of proliferation and apoptosis computed in Notebook 1.
Note that we refine this initial estimate using unbalanced optimal transport, as explained later in this notebook.

We model cellular growth with a Birth-Death Process, which assigns each cell $x$ a *rate* of division $\beta(x)$ and a  rate of death $\delta(x)$.
In a small interval of time $dt$, these *rates* imply that the probability of a division is $\beta(x)dt$, and the probability of death is $\delta(x) dt$.
The expected number of descendants after time $dt = t_2 - t_1$ is $e^{dt(\beta(x) - \delta(x))}$.

To compute the birth (i.e. division) rate and death rate, we apply a logistic function to transform the gene set scores into biologically plausible vaules for our reprogramming dataset (where the most proliferative cells double every 9 hours, and the least proliferative cells double every 60 hours).

```python
# load proliferation and apoptosis scores
gene_set_scores = pd.read_csv('data/gene_set_scores.csv', index_col=0)
proliferation=gene_set_scores['Cell.cycle']
apoptosis = gene_set_scores['Apoptosis']

# apply logistic function to transform to birth rate and death rate
def logistic(x, L, k, x0=0):
    f = L / (1 + np.exp(-k * (x - x0)))
    return f
def gen_logistic(p, beta_max, beta_min, pmax, pmin, center, width):
    return beta_min + logistic(p, L=beta_max - beta_min, k=4 / width, x0=center)

def beta(p, beta_max=1.7, beta_min=0.3, pmax=1.0, pmin=-0.5, center=0.25):
    return gen_logistic(p, beta_max, beta_min, pmax, pmin, center, width=0.5)

def delta(a, delta_max=1.7, delta_min=0.3, amax=0.5, amin=-0.4, center=0.1):
    return gen_logistic(a, delta_max, delta_min, amax, amin, center,
                          width=0.2)

birth = beta(proliferation)
death = delta(apoptosis)

# growth rate is given by
gr = np.exp(birth-death)
growth_rates_df = pd.DataFrame(index=gene_set_scores.index, data={'cell_growth_rate':gr})
growth_rates_df.to_csv('data/growth_gs_init.txt')
```

# Step 2: Compute transport maps

We are now ready to compute transport maps.

The next code block reads in the expression matrix, cell days, and the initial cell growth rates we computed in step 1 above. If the cell growth rates are not provided, the algorithm will assume each cell grows at roughly the same rate.  Note that the expression matrix we use here contains variable gene expression data of 1,400 genes from two time-course experiments, and we apply a filter to select cells from the serum time course.

```python
VAR_GENE_DS_PATH = 'data/ExprMatrix.var.genes.h5ad'
CELL_DAYS_PATH = 'data/cell_days.txt'
SERUM_CELL_IDS_PATH = 'data/serum_cell_ids.txt'
CELL_GROWTH_PATH = 'data/growth_gs_init.txt'

# load data
adata = wot.io.read_dataset(VAR_GENE_DS_PATH, obs=[CELL_DAYS_PATH, CELL_GROWTH_PATH], obs_filter=SERUM_CELL_IDS_PATH)
adata.shape
```

    (175472, 1479)

# The optimization problem

We solve the following *unbalanced transport* optimization problem introduced in Chizat et al 2018, where we only enforce the row-sum constraints approximately and we add entropy to the transport matrix:

\begin{equation}
\begin{aligned}
\underset{\pi}{\text{minimize}} & \qquad \iint c(x,y) \pi(x,y) dx dy - \epsilon \int \pi(x,y) \log \pi(x,y) dx dy \\
&\qquad + \lambda_2 {\text{KL}} \left ( \int \pi(x,y) dx  \Big \vert d \hat {\mathbb{P}}_{t_2} (y) \int g(x)^{t_2 - t_1} d \hat {\mathbb{P}}_{t_1}(x) \right ) \\
& \qquad + \lambda_1 {\text{KL}} \left ( \int \pi(x,y) dy \Big \vert d \hat {\mathbb{P}}_{t_1} (x) g(x)^{t_2 - t_1} \right).
\end{aligned}
\end{equation}

Here we use the notation  $\hat {\mathbb{P}}_{t_k} = \frac 1 n \sum_{i=1}^n \delta_{x_i}$ for the empirical distribution of samples $x_1,\ldots,x_n$ at time $t_k$, and $\text{KL}(P \vert Q)$ denotes the KL-divergence between distributions $P$ and $Q$. The function $c(x,y)$ encodes the cost of transporting a unit mass from $x$ to $y$. We define $c(x,y)$ to be the squared euclidean distance between cells in local PCA space. This PCA space is computed separately for each pair of time-points. Finally, the function $g(x)$ encodes the growth rate of cell $x$, and is used to specify the budget of descendant mass for each cell $x_i$ at time $t_1$.

The optimization problem has three regularization parameters:
* $\epsilon$ controls the degree of entropy in the transport map.
A larger value gives more entropic descendant distributions, where cells are able to obtain more fates.
* $\lambda_1$ controls the constraint on the row sums of $\pi_{t_1,t_2}$, which depend on the growth rate function $g(x)$
A smaller value of $\lambda_1$ enforces the constraints less strictly, which is useful when we do not have precise information about $g(x)$.
* $\lambda_2$ controls the constraint on the column sums of $\pi_{t_1,t_2}$.

In the following code block we initialize an OTModel, which is parameterized by $\epsilon, \lambda_1, \lambda_2$.

```python
# create OTModel
ot_model = wot.ot.OTModel(adata,epsilon = 0.05, lambda1 = 1,lambda2 = 50)
```

We can now use the OTModel to compute transport maps between any pair of time-points.

```python
# Compute a single transport map from day 7 to 7.5
tmap_annotated = ot_model.compute_transport_map(7,7.5)
```

The object tmap_annotated is an annotated transport matrix, and tmap_annotated.X contains the actual numerical matrix.
Both rows and columns are annotated with cell barcodes and cell growth rates (just along rows). These annotations are accessed as follows:

```python
# row annotations include cell growth rates
tmap_annotated.obs

# columns annotated by cell barcodes
# tmap_annotated.var
```

      g0
      g1

      index

      D7_Dox_C1_AAACCTGAGGAATCGC-1
      1.268895
      0.977589

      D7_Dox_C1_AAACCTGAGTAGATGT-1
      2.978660
      2.510199

      D7_Dox_C1_AAACCTGCAACCGCCA-1
      1.871918
      1.789104

      D7_Dox_C1_AAACCTGCACTATCTT-1
      3.072989
      2.790364

      D7_Dox_C1_AAACCTGCAGCCTGTG-1
      1.026225
      0.918584

      D7_Dox_C1_AAACCTGCATATACGC-1
      1.412582
      1.366065

      D7_Dox_C1_AAACCTGCATCCGTGG-1
      1.182596
      1.144585

      D7_Dox_C1_AAACCTGTCGGAATCT-1
      0.982687
      1.047557

      D7_Dox_C1_AAACGGGGTAGTACCT-1
      0.417277
      0.443068

      D7_Dox_C1_AAACGGGTCTCTTATG-1
      0.348761
      0.282756

      D7_Dox_C1_AAACGGGTCTGGGCCA-1
      2.636122
      2.038604

      D7_Dox_C1_AAACGGGTCTGTCAAG-1
      0.399207
      0.446110

      D7_Dox_C1_AAAGATGAGAGAGCTC-1
      1.564738
      1.329046

      D7_Dox_C1_AAAGATGAGAGTACAT-1
      1.485496
      1.384023

      D7_Dox_C1_AAAGATGAGATCCGAG-1
      0.699411
      0.789952

      D7_Dox_C1_AAAGATGAGGGCACTA-1
      1.776569
      1.709204

      D7_Dox_C1_AAAGATGCACTTAACG-1
      0.990188
      0.850556

      D7_Dox_C1_AAAGATGGTCCCTTGT-1
      3.447298
      3.750008

      D7_Dox_C1_AAAGATGTCCGCAAGC-1
      0.405894
      0.475874

      D7_Dox_C1_AAAGATGTCGCGTTTC-1
      1.291190
      1.144758

      D7_Dox_C1_AAAGATGTCGTGGACC-1
      3.467186
      2.937608

      D7_Dox_C1_AAAGCAAAGAGTACCG-1
      2.457092
      1.622737

      D7_Dox_C1_AAAGCAATCACTTATC-1
      1.255358
      1.328561

      D7_Dox_C1_AAAGTAGAGAGTTGGC-1
      1.755248
      1.891669

      D7_Dox_C1_AAAGTAGAGCTAACTC-1
      1.652005
      1.428341

      D7_Dox_C1_AAAGTAGAGGAGTACC-1
      0.543827
      0.574121

      D7_Dox_C1_AAAGTAGCAGTTTACG-1
      2.881975
      2.747883

      D7_Dox_C1_AAAGTAGCATTAACCG-1
      1.117299
      0.860455

      D7_Dox_C1_AAAGTAGGTACTCGCG-1
      2.765129
      2.744694

      D7_Dox_C1_AAAGTAGTCTCGATGA-1
      1.426713
      1.493590

      ...
      ...
      ...

      D7_Dox_C2_TTTATGCCAAACTGCT-1
      0.546127
      0.668312

      D7_Dox_C2_TTTATGCCACATGACT-1
      3.406330
      3.133376

      D7_Dox_C2_TTTATGCCATACTCTT-1
      2.149502
      1.922778

      D7_Dox_C2_TTTATGCGTGGCCCTA-1
      2.583077
      2.744961

      D7_Dox_C2_TTTATGCGTTTAAGCC-1
      3.267880
      2.653814

      D7_Dox_C2_TTTATGCTCAATAAGG-1
      0.681796
      0.808452

      D7_Dox_C2_TTTATGCTCATTATCC-1
      3.685506
      2.870278

      D7_Dox_C2_TTTATGCTCCTTGACC-1
      0.337586
      0.440907

      D7_Dox_C2_TTTATGCTCCTTTCTC-1
      2.856774
      2.429800

      D7_Dox_C2_TTTATGCTCTTGGGTA-1
      0.425422
      0.620381

      D7_Dox_C2_TTTCCTCCATCGATTG-1
      0.267748
      0.358732

      D7_Dox_C2_TTTCCTCGTAGTACCT-1
      2.577453
      2.360288

      D7_Dox_C2_TTTCCTCGTTATGCGT-1
      0.959450
      1.076300

      D7_Dox_C2_TTTCCTCTCCTTAATC-1
      3.275354
      3.232358

      D7_Dox_C2_TTTCCTCTCGATGAGG-1
      0.470076
      0.660028

      D7_Dox_C2_TTTCCTCTCTGTTTGT-1
      2.642529
      2.428539

      D7_Dox_C2_TTTGCGCTCTTACCGC-1
      0.257352
      0.273753

      D7_Dox_C2_TTTGGTTAGCGAGAAA-1
      0.294904
      0.404630

      D7_Dox_C2_TTTGGTTAGGACACCA-1
      0.512239
      0.583090

      D7_Dox_C2_TTTGGTTCAAAGGTGC-1
      1.495061
      1.318807

      D7_Dox_C2_TTTGGTTGTGATGATA-1
      2.325406
      2.188960

      D7_Dox_C2_TTTGGTTGTTGAACTC-1
      0.321137
      0.383881

      D7_Dox_C2_TTTGGTTTCGTACCGG-1
      1.524528
      1.443170

      D7_Dox_C2_TTTGGTTTCTAACGGT-1
      0.792568
      0.755125

      D7_Dox_C2_TTTGTCAAGTGTCCCG-1
      0.841414
      0.897749

      D7_Dox_C2_TTTGTCAAGTTAGCGG-1
      0.338531
      0.370486

      D7_Dox_C2_TTTGTCACAGTGGAGT-1
      0.582584
      0.609164

      D7_Dox_C2_TTTGTCAGTAATTGGA-1
      0.254926
      0.296702

      D7_Dox_C2_TTTGTCATCGTGACAT-1
      0.594715
      0.876372

      D7_Dox_C2_TTTGTCATCTCCCTGA-1
      1.198471
      1.539571

6507 rows × 2 columns

The column `g0` displays the input growth rates and the column `g1` displays the row sums of the transport map (raised to the power $t_2 - t_1$. These values can differ, as we see in the plot below, because the row-sum constraints are only enforced loosely. We interpret the value `g1` as an *inferred growth rate*.

```python
# Visualize how growth rates change with growth iterations
plt.scatter(tmap_annotated.obs['g0'],tmap_annotated.obs['g1'])
plt.xlabel("g0")
plt.ylabel("g1")
plt.title("Input vs Output Growth Rates")
plt.show()
```

When we increase the regularization parameter $\lambda_1$, the row sum constraints are enforced more strictly.

```python
ot_model_strict = wot.ot.OTModel(adata,epsilon = 0.05, lambda1 = 3,lambda2 = 50)
tmap_anno_strict = ot_model_strict.compute_transport_map(7,7.5)
```

```python
plt.scatter(tmap_anno_strict.obs['g0'],tmap_anno_strict.obs['g1'])
plt.xlabel("g0")
plt.ylabel("g1")
plt.title("Input vs Output Growth Rates")
plt.show()
```

# Updating growth estimates

We can feed these new growth rates back into the OT solver and recompute transport maps with these new growth rates. Instead of doing this manually, we can specify the number of 'growth iterations' with the flag `growth_iters`.

```python
ot_model_gr2 = wot.ot.OTModel(adata,epsilon = 0.05, lambda1 = 1,lambda2 = 50,growth_iters=2)
tmap_anno_gr2 = ot_model_gr2.compute_transport_map(7,7.5)
```

The row annotations of this transport map now have fields for multiple iterations of learning growth:

```python
# row annotations include initial cell growth rates g0, and also g1 and g2
tmap_anno_gr2.obs
```

      g0
      g1
      g2

      index

      D7_Dox_C1_AAACCTGAGGAATCGC-1
      1.268895
      0.977589
      0.771923

      D7_Dox_C1_AAACCTGAGTAGATGT-1
      2.978660
      2.510199
      2.130938

      D7_Dox_C1_AAACCTGCAACCGCCA-1
      1.871918
      1.789104
      1.692330

      D7_Dox_C1_AAACCTGCACTATCTT-1
      3.072989
      2.790364
      2.516863

      D7_Dox_C1_AAACCTGCAGCCTGTG-1
      1.026225
      0.918584
      0.815139

      D7_Dox_C1_AAACCTGCATATACGC-1
      1.412582
      1.366065
      1.303655

      D7_Dox_C1_AAACCTGCATCCGTGG-1
      1.182596
      1.144585
      1.093318

      D7_Dox_C1_AAACCTGTCGGAATCT-1
      0.982687
      1.047557
      1.093740

      D7_Dox_C1_AAACGGGGTAGTACCT-1
      0.417277
      0.443068
      0.462439

      D7_Dox_C1_AAACGGGTCTCTTATG-1
      0.348761
      0.282756
      0.228207

      D7_Dox_C1_AAACGGGTCTGGGCCA-1
      2.636122
      2.038604
      1.597445

      D7_Dox_C1_AAACGGGTCTGTCAAG-1
      0.399207
      0.446110
      0.482377

      D7_Dox_C1_AAAGATGAGAGAGCTC-1
      1.564738
      1.329046
      1.122280

      D7_Dox_C1_AAAGATGAGAGTACAT-1
      1.485496
      1.384023
      1.281136

      D7_Dox_C1_AAAGATGAGATCCGAG-1
      0.699411
      0.789952
      0.872767

      D7_Dox_C1_AAAGATGAGGGCACTA-1
      1.776569
      1.709204
      1.641042

      D7_Dox_C1_AAAGATGCACTTAACG-1
      0.990188
      0.850556
      0.728069

      D7_Dox_C1_AAAGATGGTCCCTTGT-1
      3.447298
      3.750008
      4.010355

      D7_Dox_C1_AAAGATGTCCGCAAGC-1
      0.405894
      0.475874
      0.539814

      D7_Dox_C1_AAAGATGTCGCGTTTC-1
      1.291190
      1.144758
      1.010505

      D7_Dox_C1_AAAGATGTCGTGGACC-1
      3.467186
      2.937608
      2.486141

      D7_Dox_C1_AAAGCAAAGAGTACCG-1
      2.457092
      1.622737
      1.104880

      D7_Dox_C1_AAAGCAATCACTTATC-1
      1.255358
      1.328561
      1.376567

      D7_Dox_C1_AAAGTAGAGAGTTGGC-1
      1.755248
      1.891669
      2.018037

      D7_Dox_C1_AAAGTAGAGCTAACTC-1
      1.652005
      1.428341
      1.230901

      D7_Dox_C1_AAAGTAGAGGAGTACC-1
      0.543827
      0.574121
      0.591511

      D7_Dox_C1_AAAGTAGCAGTTTACG-1
      2.881975
      2.747883
      2.580114

      D7_Dox_C1_AAAGTAGCATTAACCG-1
      1.117299
      0.860455
      0.672185

      D7_Dox_C1_AAAGTAGGTACTCGCG-1
      2.765129
      2.744694
      2.694334

      D7_Dox_C1_AAAGTAGTCTCGATGA-1
      1.426713
      1.493590
      1.543274

      ...
      ...
      ...
      ...

      D7_Dox_C2_TTTATGCCAAACTGCT-1
      0.546127
      0.668312
      0.792676

      D7_Dox_C2_TTTATGCCACATGACT-1
      3.406330
      3.133376
      2.934748

      D7_Dox_C2_TTTATGCCATACTCTT-1
      2.149502
      1.922778
      1.724100

      D7_Dox_C2_TTTATGCGTGGCCCTA-1
      2.583077
      2.744961
      2.847056

      D7_Dox_C2_TTTATGCGTTTAAGCC-1
      3.267880
      2.653814
      2.172043

      D7_Dox_C2_TTTATGCTCAATAAGG-1
      0.681796
      0.808452
      0.928986

      D7_Dox_C2_TTTATGCTCATTATCC-1
      3.685506
      2.870278
      2.267717

      D7_Dox_C2_TTTATGCTCCTTGACC-1
      0.337586
      0.440907
      0.554888

      D7_Dox_C2_TTTATGCTCCTTTCTC-1
      2.856774
      2.429800
      2.113878

      D7_Dox_C2_TTTATGCTCTTGGGTA-1
      0.425422
      0.620381
      0.875085

      D7_Dox_C2_TTTCCTCCATCGATTG-1
      0.267748
      0.358732
      0.460588

      D7_Dox_C2_TTTCCTCGTAGTACCT-1
      2.577453
      2.360288
      2.148033

      D7_Dox_C2_TTTCCTCGTTATGCGT-1
      0.959450
      1.076300
      1.177374

      D7_Dox_C2_TTTCCTCTCCTTAATC-1
      3.275354
      3.232358
      3.133913

      D7_Dox_C2_TTTCCTCTCGATGAGG-1
      0.470076
      0.660028
      0.894440

      D7_Dox_C2_TTTCCTCTCTGTTTGT-1
      2.642529
      2.428539
      2.226831

      D7_Dox_C2_TTTGCGCTCTTACCGC-1
      0.257352
      0.273753
      0.284005

      D7_Dox_C2_TTTGGTTAGCGAGAAA-1
      0.294904
      0.404630
      0.530570

      D7_Dox_C2_TTTGGTTAGGACACCA-1
      0.512239
      0.583090
      0.645941

      D7_Dox_C2_TTTGGTTCAAAGGTGC-1
      1.495061
      1.318807
      1.158094

      D7_Dox_C2_TTTGGTTGTGATGATA-1
      2.325406
      2.188960
      2.083658

      D7_Dox_C2_TTTGGTTGTTGAACTC-1
      0.321137
      0.383881
      0.442784

      D7_Dox_C2_TTTGGTTTCGTACCGG-1
      1.524528
      1.443170
      1.351901

      D7_Dox_C2_TTTGGTTTCTAACGGT-1
      0.792568
      0.755125
      0.708005

      D7_Dox_C2_TTTGTCAAGTGTCCCG-1
      0.841414
      0.897749
      0.937937

      D7_Dox_C2_TTTGTCAAGTTAGCGG-1
      0.338531
      0.370486
      0.397700

      D7_Dox_C2_TTTGTCACAGTGGAGT-1
      0.582584
      0.609164
      0.624181

      D7_Dox_C2_TTTGTCAGTAATTGGA-1
      0.254926
      0.296702
      0.334487

      D7_Dox_C2_TTTGTCATCGTGACAT-1
      0.594715
      0.876372
      1.247693

      D7_Dox_C2_TTTGTCATCTCCCTGA-1
      1.198471
      1.539571
      1.896885

6507 rows × 3 columns

# Tuning $\lambda_1$ and $\lambda_2$

The numerical value of $\lambda_1$ determines how different $g_{k+1}$ can be from $g_k$. If growth rates are known very well, then a large value of $\lambda_1$ can be used to enforce the row-sum constraints strictly and we could just do one growth iteration. When we do more than one growth iteration, the value of $\lambda_1$ can be interpreted intuitively as an *inverse step size* in our update of growth rates. A large value allows us to change growth by a lot at each iteration.

You may have noticed that we set $\lambda_2 = 50$, a much larger numerical value than $\lambda_1 = 1$. This enforces the constraints on the column sums strictly (the amount of mass going into each cell $y$ at time $t_2$ is well specified). To see this, we plot a histogram of column sums, and we see a very narrow band.

```python
colsums = tmap_anno_gr2.X.sum(axis=0);
plt.hist(colsums)
```

# Tuning $\epsilon$

The parameter $\epsilon$ controls the level of entropy in the transport maps. A larger value promotes more entropic descendant distributions, so each cell can transition to a larger variety of descendants. We recommend sticking with the default value of $\epsilon = 0.05$. Note that we rescale the cost matrix to have a median value of $1,$ so the default value of $\epsilon$ should give similar results for datasets of different scales.

# Compute all transport maps in batch mode

The following command computes all transport maps. Note that this can take a few hours on a laptop. For convenience, we include precomputed transport maps in the `tmaps` directory.

```python
# ot_model = wot.ot.OTModel(adata, epsilon = 0.05, lambda1 = 1, lambda2 = 50, growth_iters = 3)
# ot_model.compute_all_transport_maps(tmap_out='tmaps/serum')

# we can speed this up by supplying g2 and doing 1 growth iter
```

This can also be run from the command line as follows:

`wot optimal_transport --matrix data/ExprMatrix.var.genes.h5ad --cell_days data/cell_days.txt --cell_filter data/serum_cell_ids.txt --cell_growth_rates data/growth_gs_init.txt --growth_iters 3 --lambda1 1 --lambda2 50 --epsilon 0.05`

We use these transport maps in the remaining notebooks.


## Non-massaction propensities in BioCRNpyler
The propensity $\rho(s)$ (or rate function) of a chemical reaction determines how quickly that reaction occurs. It is assumed to be a function of the chemical species $s$ as well as internal parameter specific to the propensity type.

\begin{equation}
\{\textrm{Input Species}\} \xrightarrow{\rho(s)} \{\textrm{Output Species}\}
\end{equation}

By default, BioCRNpyler assumes that propensities are massaction with only one parameter, the rate constant $k$. However, non-massaction propensities are also supported. When creating a reaction with non-massaction propensities, a propensity_params dictionary must be passed into the chemical_reaction_network.reaction constructor with the appropriate parameters for that particular propensity type. The supported propensity types and their relevant parameters are:

1. "massaction: $\rho(S) = k \Pi_{s} s^{I_s}$. Required parameters: "k" the rate constant. Note: for stochastic models mass action propensities are $\rho(S) = k \Pi_{s} s!/(s - I_s)!$.
2. "positivehill": $\rho(s) = k \frac{s^n}{(K^n+s^n)}$. Requried parameters: rate constant "k", offset "K", hill coefficient "n", hill species "s1".
3. "negativehill": $\rho(s) = k \frac{1}{(K^n+s^n)}$. Requried parameters: rate constant "k", offset "K", hill coefficient "n", hill species "s1".
4. "proportionalpositivehill": $\rho(s) = k d \frac{s^n}{(K^n+s^n)}$. Requried parameters: rate constant "k", offset "K", hill coefficient "n", hill species "s1", propritional species "d".
5. "proportionalnegativehill": $\rho(s) = k d \frac{1}{(K^n+s^n)}$. Requried parameters: rate constant "k", offset "K", hill coefficient "n", hill species "s1", propritional species "d".
6. "general": $\rho(s) = f(s)$ where $f$ can be any algebraic function typed as a string. Required parameters: "rate" an algebraic expression including species and model parameters written as a string.

The following notebook contains examples of the propensity types support by BioCRNpyler.

```python
from biocrnpyler.chemical_reaction_network import Species, Reaction, ComplexSpecies, ChemicalReactionNetwork

#Names of different supported propensities
propensity_types = ['hillpositive', 'proportionalhillpositive', 'hillnegative', 'proportionalhillnegative', 'massaction', 'general']

#Parameter Values
kb = 100
ku = 10
kex = 1.
kd = .1

#Species
G = Species(name = "G", material_type = "dna") #DNA
A = Species(name = "A", material_type = "protein") #Activator
GA = ComplexSpecies([G, A, A]) #Activated Gene
X = Species(name = "X", material_type = "protein")

rxnd = Reaction([X], [], kd)

#Massaction Activation
species1 = [G, A, GA, X]
rxn0_1 = Reaction([G, A, A], [GA], k=kb, k_rev = ku)
rxn0_2 = Reaction([GA], [GA, X], k=kex)
CRN0 = ChemicalReactionNetwork(species1, [rxn0_1, rxn0_2, rxnd])
print("\nMassaction Activation", repr(CRN0))

#Massaction Repressed
rxn1_1 = Reaction([G, A, A], [GA], k=kb, k_rev = ku)
rxn1_2 = Reaction([G], [G, X], k=kex)
CRN1 = ChemicalReactionNetwork(species1, [rxn1_1, rxn1_2, rxnd])
print("\nMassaction Repression", repr(CRN1))

#hill positive
species2 = [G, A, X]
rxn2_1 = Reaction([G], [G, X], propensity_type = "hillpositive", propensity_params = {"k":kex, "n":2, "K":float(kb/ku), "s1":A})
CRN2 = ChemicalReactionNetwork(species2, [rxn2_1, rxnd])
print("\nHill Positive CRN", repr(CRN2))

#proportional hill positive
rxn3_1 = Reaction([G], [G, X], propensity_type = "proportionalhillpositive", propensity_params = {"k":kex, "n":2, "K":float(kb/ku), "s1":A, "d":G})
CRN3 = ChemicalReactionNetwork(species2, [rxn3_1, rxnd])
print("\nProportional Hill Positive CRN", repr(CRN3))

#hill Negative
rxn4_1 = Reaction([G], [G, X], propensity_type = "hillnegative", propensity_params = {"k":kex, "n":2, "K":float(kb/ku), "s1":A})
CRN4 = ChemicalReactionNetwork(species2, [rxn4_1, rxnd])
print("\nHill Negative CRN", repr(CRN4))

#proportional hill negative
rxn5_1 = Reaction([G], [G, X], propensity_type = "proportionalhillnegative", propensity_params = {"k":kex, "n":2, "K":float(kb/ku), "s1":A, "d":G})
CRN5 = ChemicalReactionNetwork(species2, [rxn5_1, rxnd])
print("\nProportional Hill Negative CRN", repr(CRN5))
```

    Massaction Activation Species = dna_G, protein_A, complex_protein_A_protein_A_dna_G, protein_X
    Reactions = [
    	dna_G + 2*protein_A <--> complex_protein_A_protein_A_dna_G        massaction: k_f(dna_G,protein_A)=100*dna_G*protein_A^2        k_r(complex_protein_A_protein_A_dna_G)=10*complex_protein_A_protein_A_dna_G
    	complex_protein_A_protein_A_dna_G --> complex_protein_A_protein_A_dna_G + protein_X        massaction: k_f(complex_protein_A_protein_A_dna_G)=1.0*complex_protein_A_protein_A_dna_G
    	protein_X -->         massaction: k_f(protein_X)=0.1*protein_X
    ]

    Massaction Repression Species = dna_G, protein_A, complex_protein_A_protein_A_dna_G, protein_X
    Reactions = [
    	dna_G + 2*protein_A <--> complex_protein_A_protein_A_dna_G        massaction: k_f(dna_G,protein_A)=100*dna_G*protein_A^2        k_r(complex_protein_A_protein_A_dna_G)=10*complex_protein_A_protein_A_dna_G
    	dna_G --> dna_G + protein_X        massaction: k_f(dna_G)=1.0*dna_G
    	protein_X -->         massaction: k_f(protein_X)=0.1*protein_X
    ]

    Hill Positive CRN Species = dna_G, protein_A, protein_X
    Reactions = [
    	dna_G --> dna_G + protein_X        hillpositive: k(protein_A)=1.0*protein_A^2/(10.0+protein_A^2)
    	protein_X -->         massaction: k_f(protein_X)=0.1*protein_X
    ]

    Proportional Hill Positive CRN Species = dna_G, protein_A, protein_X
    Reactions = [
    	dna_G --> dna_G + protein_X        proportionalhillpositive: k(protein_A, dna_G)=1.0*dna_G*protein_A^2/(10.0+protein_A^2)
    	protein_X -->         massaction: k_f(protein_X)=0.1*protein_X
    ]

    Hill Negative CRN Species = dna_G, protein_A, protein_X
    Reactions = [
    	dna_G --> dna_G + protein_X        hillnegative: k(protein_A)=1.0*1/(10.0+protein_A^2)
    	protein_X -->         massaction: k_f(protein_X)=0.1*protein_X
    ]

    Proportional Hill Negative CRN Species = dna_G, protein_A, protein_X
    Reactions = [
    	dna_G --> dna_G + protein_X        proportionalhillnegative: k(protein_A, dna_G)=1.0*dna_G/(10.0+protein_A^2)
    	protein_X -->         massaction: k_f(protein_X)=0.1*protein_X
    ]

```python

```

```python

```


# Problem 3.5 Sampling from an SCM

Given the SCM
\begin{align}
X:=&Y^2+N_X\\
Y:=&N_Y
\end{align}
with $N_X,N_Y\sim\mathcal{N}\left(0,1\right)$ generate an i.i.d. sample of size 200 from the joint distribution $(X,Y)$

```python
import numpy as np
```

```python
mu = 0
sigma = 1
n = 200
```

```python
n_y = np.random.normal(mu, sigma, 200)
```

```python
n_x = np.random.normal(mu, sigma, 200)
```

```python
y = n_y
```

```python
x = y**2 + n_x
```

```python
np.array((x, y)).T
```

    array([[ 2.88976307e-01, -8.23220136e-01],
           [ 1.59448997e+00, -1.38773832e-01],
           [-5.72960776e-01,  5.62835648e-02],
           [ 3.49708474e+00, -1.19241728e+00],
           [ 1.90208107e+00,  7.91060381e-01],
           [ 6.55350477e-01,  9.90000828e-01],
           [ 1.78292061e+00,  1.66744188e+00],
           [ 5.42209001e-01,  1.36250529e+00],
           [-3.19743095e-01,  9.69363942e-01],
           [-1.65213657e+00,  5.51319327e-01],
           [ 1.59112885e+00, -7.99451442e-01],
           [-2.32134554e-01, -3.55704223e-01],
           [ 2.66601023e-01,  2.11733094e-01],
           [ 7.88183011e-01,  8.79769336e-01],
           [ 3.21860175e+00, -1.75805835e+00],
           [ 1.80631701e+00,  8.66089799e-01],
           [ 2.46703793e+00,  4.21970048e-01],
           [-1.14852274e-01, -1.68514627e-01],
           [ 6.67279672e-01, -1.99682690e-01],
           [ 1.74534538e-01, -9.20782894e-01],
           [ 2.44086990e-01,  8.29891392e-01],
           [-1.24663709e-01, -4.22908401e-02],
           [-9.40233251e-01, -5.91065426e-01],
           [ 6.98912512e-01, -5.82331327e-01],
           [ 7.33164997e-01,  3.23368335e-01],
           [ 7.18955659e+00,  2.36708513e+00],
           [ 5.85148417e-01,  3.08344439e-01],
           [ 1.12781652e+00,  1.09401313e+00],
           [ 3.02942397e+00, -1.76133665e+00],
           [ 2.66566736e+00, -1.52284549e+00],
           [ 8.03233568e-01,  6.41859029e-01],
           [-6.77450518e-01,  6.89704226e-01],
           [-2.96765925e-01, -4.21117747e-01],
           [ 1.71830893e+00, -8.96744983e-01],
           [-1.18132707e+00,  1.01028200e+00],
           [ 1.28797039e-03, -3.78562511e-01],
           [ 7.53968517e-01, -7.14599686e-02],
           [ 9.72837049e-01,  6.91957921e-01],
           [ 3.92813750e-01,  6.39521421e-01],
           [-7.06019962e-01, -7.79920702e-01],
           [ 4.32204035e-01,  2.12373165e-01],
           [ 3.65131881e+00, -1.64440952e+00],
           [-2.98328102e-01,  7.56816491e-01],
           [ 3.73691023e-01,  4.40495038e-01],
           [ 2.15415561e+00,  1.74574479e+00],
           [-1.67306832e+00, -4.64313199e-01],
           [-3.88015156e-01, -5.44212331e-01],
           [ 6.30947184e-01,  4.00303690e-01],
           [ 1.80589833e+00,  1.08346750e+00],
           [ 3.62322234e+00, -1.83916242e+00],
           [-1.27624250e+00,  8.96883304e-01],
           [ 5.53654475e-01, -3.61462352e-02],
           [-1.31858540e+00, -1.56542731e-01],
           [ 1.96798942e+00,  1.26753361e+00],
           [ 9.49114950e-01, -3.22381679e-01],
           [ 2.16744342e-01,  7.34459175e-02],
           [ 1.07375760e+00,  3.82371303e-01],
           [ 6.83347597e-01,  7.95597552e-01],
           [-1.11971534e-01,  6.58526883e-01],
           [ 7.46348945e-01, -8.37716390e-01],
           [ 5.80770052e-02,  1.58101126e-01],
           [ 7.97514409e-02, -5.99220465e-01],
           [ 1.83711355e+00,  1.18689934e+00],
           [-5.23888682e-01, -1.25965717e-01],
           [ 3.86048294e+00,  1.70356039e+00],
           [ 1.26755985e+00, -1.11517632e+00],
           [-1.09745086e+00, -1.05344911e-01],
           [ 5.64105294e-01,  1.02928790e+00],
           [-1.67437580e+00, -7.47480095e-02],
           [ 8.21446967e-01,  6.79620651e-01],
           [ 1.16359284e+00, -1.31943979e+00],
           [ 6.27997056e-01, -9.04894703e-01],
           [-4.24315707e-01,  1.33840687e+00],
           [ 1.07567051e+00,  6.88755824e-01],
           [-8.66839740e-01,  9.84541017e-01],
           [-1.12072522e+00,  4.05137952e-01],
           [ 2.51544914e-02,  4.37980153e-01],
           [ 1.67564082e+00, -1.43794977e+00],
           [ 7.67390544e+00,  2.45986315e+00],
           [ 5.54131613e+00, -2.29641703e+00],
           [-6.94135424e-01, -2.65053409e-02],
           [-6.81114836e-01, -2.72844238e-01],
           [ 9.49903239e-01, -1.06428228e+00],
           [ 3.18554104e+00,  1.90624864e+00],
           [ 2.79022666e+00, -8.18027607e-01],
           [-5.25588448e-01, -9.15852860e-03],
           [ 5.04508771e-01,  7.14760209e-01],
           [ 1.73334572e+00, -5.78057022e-01],
           [-3.18308662e-01, -6.12994669e-02],
           [ 1.90080776e+00, -9.73386615e-01],
           [-1.15971359e+00,  6.68859621e-01],
           [-1.85095524e-01,  2.58586725e-01],
           [-4.85630170e-02, -1.08332248e+00],
           [ 5.74025955e+00, -2.09686000e+00],
           [ 1.07575966e+00,  3.12943825e-01],
           [ 4.40494544e+00,  1.89823161e+00],
           [ 2.76642521e-01, -6.58559145e-02],
           [ 3.12190725e-01, -2.43129676e-01],
           [ 2.62696818e-01, -3.92288840e-01],
           [-2.95926372e-01,  9.54393690e-02],
           [ 1.85901464e+00,  7.38452125e-01],
           [-8.84479213e-02, -1.30823781e+00],
           [ 4.62880742e+00, -1.66000288e+00],
           [ 9.26526427e-01,  9.27105821e-01],
           [ 2.88684269e-01, -1.48378491e-01],
           [ 1.57289147e+00,  7.32827959e-01],
           [-1.70031198e+00, -1.88501633e-01],
           [-1.48068623e-01, -2.70106798e-01],
           [-1.63704578e+00,  4.37409482e-02],
           [ 1.44866196e+00, -1.66882428e+00],
           [ 5.13849279e-01, -4.99348381e-01],
           [ 1.62767969e+00, -5.29640967e-01],
           [ 2.75827898e+00, -8.66199602e-01],
           [ 2.72306672e+00,  1.30700102e+00],
           [-5.88998087e-01, -1.23495753e-01],
           [-3.07406628e-01,  2.82384343e-01],
           [-4.53202471e-01, -4.29433784e-01],
           [ 1.79134562e+00,  1.11448967e+00],
           [ 1.65684792e-01,  1.72283551e-01],
           [ 2.04561639e+00, -1.19475323e+00],
           [ 2.26648792e+00,  9.62784933e-01],
           [-4.27153082e-01,  1.55152564e-01],
           [-1.80417945e-01, -7.06103117e-01],
           [ 8.68577870e-01,  1.03413385e-01],
           [ 3.92437558e-02, -3.59332987e-01],
           [ 1.88988904e+00, -1.40236290e+00],
           [-4.94055723e-01, -4.66876466e-02],
           [-5.19742537e-01,  4.69214425e-01],
           [-5.84388648e-01, -3.78220761e-01],
           [-3.66407346e-01, -5.72231417e-01],
           [ 1.83499816e+00,  1.01516688e+00],
           [ 2.11578763e+00,  1.46251130e+00],
           [ 4.83711985e+00,  1.92842858e+00],
           [-8.98228765e-01, -9.95054806e-01],
           [ 4.23862685e-01, -1.44145983e-01],
           [ 9.76017432e-01, -1.34665249e-01],
           [ 2.42545633e-01, -1.63163414e-01],
           [-7.41806317e-01,  2.84694561e-01],
           [ 6.30915821e+00, -2.69643453e+00],
           [ 2.74083350e+00,  1.31595483e+00],
           [ 1.18352151e+00,  4.06929244e-01],
           [ 2.40875591e+00, -1.58086455e+00],
           [ 4.28736283e+00, -2.07146271e+00],
           [-7.51241667e-01, -6.77600295e-01],
           [ 3.57770438e+00,  8.57624689e-01],
           [ 1.05860096e+00, -6.90449490e-01],
           [-7.19207933e-01,  2.42917632e-02],
           [ 2.83463355e+00,  1.55309633e+00],
           [ 9.64597493e-02, -5.56491922e-01],
           [ 1.09504263e+00,  7.01803361e-01],
           [ 3.73341691e+00,  1.86214218e+00],
           [ 1.97804408e-01, -3.23609659e-01],
           [ 5.84154192e-01,  5.89455641e-01],
           [ 3.61247381e+00, -1.27888824e+00],
           [-1.65365757e-01, -1.05712753e-01],
           [ 1.96564953e-01,  1.51700172e+00],
           [ 3.29687094e+00,  1.24759616e+00],
           [ 2.85065096e-01,  8.10558101e-01],
           [ 3.95292636e-01, -7.90947527e-01],
           [ 1.24630054e+00, -1.07035284e+00],
           [-8.61565140e-01,  4.63021516e-01],
           [-9.23203040e-02,  2.94071982e-01],
           [-1.08250950e+00,  8.37967669e-02],
           [ 1.80596149e+00,  1.03627504e+00],
           [ 6.91611371e+00, -2.55224492e+00],
           [ 3.06416725e-01,  4.19937121e-01],
           [ 5.91250128e-01,  1.00477729e+00],
           [ 3.76265942e-02,  5.18994792e-01],
           [-6.97543658e-01,  4.83699817e-02],
           [ 4.66193650e+00,  1.52507657e+00],
           [-8.87980976e-02, -1.55818381e-01],
           [ 4.20617302e-01, -3.19898452e-01],
           [ 1.10356737e+00, -1.52592003e-01],
           [ 7.66094744e-01, -1.34564676e+00],
           [-1.57836276e-01, -4.81430966e-01],
           [ 5.37858011e-01, -9.92865242e-01],
           [-4.95830817e-01,  4.11161748e-01],
           [-9.44815789e-01,  6.06026867e-01],
           [ 2.78264682e+00, -1.86942406e+00],
           [ 4.63665678e-01,  1.09986768e+00],
           [ 2.05901128e-02,  2.43547074e-01],
           [ 7.33308988e-01,  4.72132803e-01],
           [-3.99496608e-02,  5.83718779e-01],
           [ 1.70580001e+00,  4.26732276e-01],
           [ 1.63347341e+00, -8.24041382e-01],
           [ 4.46344755e-02,  3.21590669e-01],
           [ 8.96892900e-01,  1.23228909e+00],
           [ 8.76189418e-01, -3.79182226e-02],
           [ 5.06288515e+00, -2.07695758e+00],
           [-1.00810958e-01, -6.98584285e-02],
           [-3.68738106e-01,  2.10500334e-01],
           [-7.52365100e-01,  2.27136446e-01],
           [ 1.23498891e+00,  1.22242171e+00],
           [-4.39112485e-01,  2.44763830e-02],
           [ 6.10924173e-01, -6.73284219e-01],
           [ 2.35747350e+00, -1.44306848e+00],
           [ 4.01945789e+00,  1.75380778e+00],
           [ 1.14837127e+00,  1.24310992e+00],
           [ 1.17527891e+00, -5.05105033e-01],
           [ 8.70058186e-01,  2.55547729e-01]])