#+PROPERTY: header-args:python :kernel python3 :pandoc t
#+PROPERTY: header-args :outputs both :results output :exports both
#+OPTIONS: toc:nil num:nil

* How to use this file                                                  :noexport:
=C-c C-e r r= to export to usage.rst file, which will be used by sphinx.
When prompted start R in docs/tutorials.

* Fitting
Requires:
- r-4.1.3-1

** Conventions and best approaches
- S0 Signal for unbound state
- S1 Signal for bound state
- K equilibrium constant (Kd or pKa)
- order data from unbound to bound (e.g. cl: 0–>150 mM; pH 9–>5)

- lmfit.Model has very convenient results plot functionalities and the unique possibility to estimate upper lower fitting curves.
- R nls and nlsboot seems very convenient (Q-Q plt) and fast; nls can perform global fit but nlstools can not.

** Initial imports
#+begin_src python
import numpy as np
import scipy
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sb
import rpy2
from rpy2.robjects import r
from rpy2.robjects.packages import importr
from rpy2.robjects import globalenv
from rpy2.robjects import pandas2ri
import lmfit

pandas2ri.activate()
%load_ext rpy2.ipython

MASS = importr('MASS')
#r('library(MASS)')
#+end_src

** Single Cl titration.


#+begin_src python :exports both :file ../_static/f01.png
df = pd.read_table("../../tests/data/copyIP.txt")
sb.scatterplot(data=df, x="cl", y="F", hue=df.cl*df.F, palette="crest", s=200, alpha=.8, legend=False)
#+end_src

*** Using R
:PROPERTIES:
:header-args:R: :session "global" :exports both
:END:

#+begin_src  R
d <- read.delim("../../tests/data/copyIP.txt")
fit = nls(F ~ (S0 + S1 * cl / Kd)/ (1 + cl / Kd), start = list(S0=7e7, S1=0, Kd=12), data=d)
summary(fit)
#+end_src

#+begin_src R
confint(fit)
#+end_src

#+begin_src R
library(nlstools)
set.seed(4)
nb = nlsBoot(fit, niter=999)
summary(nb)
#+end_src

#+begin_src R :results output graphics file :file ../_static/r_bs.png
plot(nb)
#+end_src

*** using rpy2
:PROPERTIES:
:header-args: :exports both
:END:

#+begin_src python
globalenv['Rdf'] = df

fit = rpy2.robjects.r('nls(F ~ (S0 + S1 * cl / Kd)/ (1 + cl / Kd), start = list(S0=7e7, S1=0, Kd=12), data=Rdf) ')
globalenv['rfit'] = fit
print(r.summary(fit))
print(r.confint(fit))
%R print("")
%R print(confint(rfit))
#+end_src
With older versions Rpy2 output looked nicer

#+begin_src python
print(MASS.confint_nls(fit, 'Kd'))
print(rpy2.robjects.r('summary(rfit)'))
#+end_src

#+begin_src python
nlstools = importr('nlstools')
base = importr('base')
base.set_seed(4)
nb = nlstools.nlsBoot(fit, niter=999)
globalenv['nb'] = nb
globalenv['fit'] = fit
#+end_src

#+begin_src python :file ../_static/rpy_bs.png
%%R
plot(nb)
summary(nb)
#+end_src
*** lmfit
#+begin_src python :file ../_static/lmfit1.png
import lmfit

def residual(pars, x, y=None):
    S0 =  pars['S0']
    S1 =  pars['S1']
    Kd = pars['Kd']
    model = (S0 + S1 * x / Kd) / (1 + x / Kd)
    if y is None:
        return model
    return (y - model)

params = lmfit.Parameters()
params.add('S0', value=df.F[0])
params.add('S1', value=100)
params.add('Kd', value=50, vary=True)

out = lmfit.minimize(residual, params, args=(df.cl, df.F,))

xdelta = (df.cl.max() - df.cl.min()) / 500
xfit = np.arange(df.cl.min() - xdelta, df.cl.max() + xdelta, xdelta)
yfit = residual(out.params, xfit)
print(lmfit.fit_report(out.params))
plt.plot(df.cl, df.F, "o", xfit, yfit, "-")
#+end_src

#+begin_src python
import lmfit
def residuals(p):
    S0 =  p['S0']
    S1 =  p['S1']
    Kd = p['Kd']
    model = (S0 + S1 * df.cl / Kd) / (1 + df.cl / Kd)
    return (model - df.F)

mini = lmfit.Minimizer(residuals, params)
res = mini.minimize()
ci, tr = lmfit.conf_interval(mini, res, sigmas=[.68, .95], trace=True)
print(lmfit.ci_report(ci, with_offset=False, ndigits=2))
print(lmfit.fit_report(res, show_correl=False, sort_pars=True))
#+end_src

#+begin_src python :file ../_static/lmfit2.png
names = res.params.keys()
i = 0
gs = plt.GridSpec(4, 4)
sx = {}
sy = {}
for fixed in names:
    j = 0
    for free in names:
        if j in sx and i in sy:
            ax = plt.subplot(gs[i, j], sharex=sx[j], sharey=sy[i])
        elif i in sy:
            ax = plt.subplot(gs[i, j], sharey=sy[i])
            sx[j] = ax
        elif j in sx:
            ax = plt.subplot(gs[i, j], sharex=sx[j])
            sy[i] = ax
        else:
            ax = plt.subplot(gs[i, j])
            sy[i] = ax
            sx[j] = ax
        if i < 3:
            plt.setp(ax.get_xticklabels(), visible=True)
        else:
            ax.set_xlabel(free)

        if j > 0:
            plt.setp(ax.get_yticklabels(), visible=False)
        else:
            ax.set_ylabel(fixed)

        rest = tr[fixed]
        prob = rest['prob']
        f = prob < 0.96

        x, y = rest[free], rest[fixed]
        ax.scatter(x[f], y[f], c=1-prob[f], s=25*(1-prob[f]+0.5))
        ax.autoscale(1, 1)
        j += 1
    i += 1
#+end_src

#+begin_src python :file ../_static/lmfit3.png
names = list(res.params.keys())

plt.figure()
for i in range(3):
    for j in range(3):
        indx = 9-j*3-i
        ax = plt.subplot(3, 3, indx)
        ax.ticklabel_format(style='sci', scilimits=(-2, 2), axis='y')

        # set-up labels and tick marks
        ax.tick_params(labelleft=False, labelbottom=False)
        if indx in (1, 4, 7):
            plt.ylabel(names[j])
            ax.tick_params(labelleft=True)
        if indx == 1:
            ax.tick_params(labelleft=True)
        if indx in (7, 8, 9):
            plt.xlabel(names[i])
            ax.tick_params(labelbottom=True)
            [label.set_rotation(45) for label in ax.get_xticklabels()]

        if i != j:
            x, y, m = lmfit.conf_interval2d(mini, res, names[i], names[j], 20, 20)
            plt.contourf(x, y, m, np.linspace(0, 1, 10))

            x = tr[names[i]][names[i]]
            y = tr[names[i]][names[j]]
            pr = tr[names[i]]['prob']
            s = np.argsort(x)
            plt.scatter(x[s], y[s], c=pr[s], s=30, lw=1)

        else:
            x = tr[names[i]][names[i]]
            y = tr[names[i]]['prob']

            t, s = np.unique(x, True)
            f = scipy.interpolate.interp1d(t, y[s], 'slinear')
            xn = np.linspace(x.min(), x.max(), 50)
            plt.plot(xn, f(xn), lw=1)
            plt.ylabel('prob')
            ax.tick_params(labelleft=True)

plt.tight_layout()
#+end_src

#+begin_src python :file ../_static/lmfit4.png
lmfit.report_fit(out.params, min_correl=0.25)

ci, trace = lmfit.conf_interval(mini, res, sigmas=[1, 2], trace=True)
lmfit.printfuncs.report_ci(ci)

fig, axes = plt.subplots(2, 2, figsize=(12.8, 9.6), sharey=True)
cx1, cy1, prob = trace['S0']['S0'], trace['S0']['Kd'], trace['S0']['prob']
cx2, cy2, prob2 = trace['S1']['S1'], trace['S1']['Kd'], trace['S1']['prob']

axes[0][0].scatter(cx1, cy1, c=prob, s=30)
axes[0][0].set_xlabel('S0')
axes[0][0].set_ylabel('Kd')

axes[0][1].scatter(cx2, cy2, c=prob2, s=30)
axes[0][1].set_xlabel('S1')

cx, cy, grid = lmfit.conf_interval2d(mini, res, 'S0', 'Kd', 30, 30)
ctp = axes[1][0].contourf(cx, cy, grid, np.linspace(0, 1, 11))
fig.colorbar(ctp, ax=axes[1][0])
axes[1][0].set_xlabel('S0')
axes[1][0].set_ylabel('Kd')

cx, cy, grid = lmfit.conf_interval2d(mini, res, 'S1', 'Kd', 30, 30)
ctp = axes[1][1].contourf(cx, cy, grid, np.linspace(0, 1, 11))
fig.colorbar(ctp, ax=axes[1][1])
axes[1][1].set_xlabel('S1')
axes[1][1].set_ylabel('Kd')
#+end_src
#+begin_src python :file ../_static/lmfit5.png
  x, y, grid = lmfit.conf_interval2d(mini, res, 'S0','S1', 30, 30)
  plt.contourf(x, y, grid, np.linspace(0,1,11))
  plt.xlabel('S0')
  plt.colorbar()
  plt.ylabel('S1')
#+end_src

** Notes
You could implement global fitting using scipy.leastq but will sometime fail in bootstrapping.
lmfit resulted much more robust

#+begin_src python :exports code
def fit_pH_global(fz, x, dy1, dy2):
    """Fit 2 dataset (x, y1, y2) with a single protonation site model
    """
    y1 = np.array(dy1)
    y2 = np.array(dy2)

    def ssq(p, x, y1, y2):
        return np.r_[y1 - fz(p[0], p[1:3], x), y2 - fz(p[0], p[3:5], x)]
    p0 = np.r_[x[2], y1[0], y1[-1], y2[0], y2[-1]]
    p, cov, info, msg, success = optimize.leastsq(ssq, p0, args=(x, y1, y2),
                                                  full_output=True, xtol=1e-11)
    res = namedtuple("Result", "success msg df chisqr K sK SA_1 sSA_1 \
                     SB_1 sSB_1 SA_2 sSA_2 SB_2 sSB_2")
    res.msg = msg
    res.success = success
    if 1 <= success <= 4:
        chisq = sum(info['fvec'] * info['fvec'])
        res.df = len(y1) + len(y2) - len(p)
        res.chisqr = chisq / res.df
        res.K = p[0]
        #res.sK = np.sqrt(cov[0][0] * res.chisqr)
        res.SA_1 = p[1]
        #res.sSA_1 = np.sqrt(cov[1][1] * res.chisqr)
        res.SB_1 = p[2]
        #res.sSB_1 = np.sqrt(cov[2][2] * res.chisqr)
        res.SA_2 = p[3]
        #res.sSA_2 = np.sqrt(cov[3][3] * res.chisqr)
        res.SB_2 = p[4]
        #res.sSB_2 = np.sqrt(cov[4][4] * res.chisqr)
    return res

result = fit_pH_global(fz, df.x, df.y1, df.y2)
#+end_src
