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

* init                                                                  :noexport:
#+begin_src python
import numpy as np
import scipy
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sb
import lmfit
#+end_src

* Fit titration with multiple ts
For example data collected with multiple labelblocks in Tecan plate reader.

“A01”: pH titration with y1 and y2.

#+begin_src python :results value
df = pd.read_csv('../../tests/data/A01.dat', sep=' ', names=['x', 'y1', 'y2'])
df = df[::-1].reset_index(drop=True)
df
#+end_src

** lmfit of single y1 using analytical Jacobian

It computes the Jacobian of the fz. Mind that the residual (i.e. y - fz) will be actually minimized.

#+begin_src python
import sympy
x, S0_1, S1_1, K = sympy.symbols('x S0_1 S1_1 K')
f = (S0_1 + S1_1 * 10 ** (K - x)) / (1 + 10 ** (K - x))
print(sympy.diff(f, S0_1))
print(sympy.diff(f, S1_1))
print(sympy.diff(f, K))
#+end_src

#+begin_src python
f2 = (S0_1 + S1_1 * x / K) / (1 + x / K)
print(sympy.diff(f2, S0_1))
print(sympy.diff(f2, S1_1))
print(sympy.diff(f2, K))
#+end_src

#+begin_src python
def residual(pars, x, data):
    S0 =  pars['S0']
    S1 =  pars['S1']
    K = pars['K']
    #model = (S0 + S1 * x / Kd) / (1 + x / Kd)
    x = np.array(x)
    y = np.array(data)
    model = (S0 + S1 * 10 ** (K - x)) / (1 + 10 ** (K - x))
    if data is None:
        return model
    return (y - model)

# Try Jacobian
def dfunc(pars, x, data=None):
    S0_1 =  pars['S0']
    S1_1 =  pars['S1']
    K = pars['K']
    kx = np.array(10**(K - x))
    return np.array([-1 / (kx + 1),
                     -kx / (kx + 1),
                     -kx * np.log(10) * (S1_1 / (kx + 1) - (kx * S1_1 + S0_1) / (kx + 1)**2)])
                     # kx * S1_1 * np.log(10) / (kx + 1) - kx * (kx * S1_1 + S0_1) * np.log(10) / (kx + 1)**2])

params = lmfit.Parameters()
params.add('S0', value=25000, min=0.0)
params.add('S1', value=50000, min=0.0)
params.add('K', value=7, min=2.0, max=12.0)

# out = lmfit.minimize(residual, params, args=(df.x,), kws={'data':df.y1})
# mini = lmfit.Minimizer(residual, params, fcn_args=(df.x, df.y2))
mini = lmfit.Minimizer(residual, params, fcn_args=(df.x,), fcn_kws={'data':df.y1})
# res= mini.minimize()
res= mini.leastsq(Dfun=dfunc, col_deriv=True, ftol=1e-8)

fit = residual(params, df.x, None)
print(lmfit.report_fit(res))

ci = lmfit.conf_interval(mini, res, sigmas=[1, 2, 3])
lmfit.printfuncs.report_ci(ci)
#+end_src

#+begin_src python
print(lmfit.ci_report(ci, with_offset=False, ndigits=2))
#+end_src

*** emcee                                                               :noexport:
:PROPERTIES:
:header-args: :eval never-export
:END:
#+begin_src python
res.params.add('__lnsigma', value=np.log(.1), min=np.log(0.001), max=np.log(1e4))
resMC = lmfit.minimize(residual, method='emcee', steps=3000,
                        nan_policy='omit', is_weighted=False, burn=300, thin=1,
                       params=res.params, args=(df.x, df.y1), progress=True)
#+end_src

#+begin_src python
plt.plot(resMC.acceptance_fraction, 'o')
plt.xlabel('walker')
plt.ylabel('acceptance frac')
#+end_src

#+begin_src python
import corner

tr = [v for v in resMC.params.valuesdict().values()]
emcee_plot = corner.corner(resMC.flatchain, labels=resMC.var_names,
                            truths=list(resMC.params.valuesdict().values()))
                            # truths=tr[:-1])
#+end_src

** global
I believe I was using scipy.optimize.

*** using lmfit with np.r_ trick

#+begin_src python
# %%timeit #62ms
def residual2(pars, x, data=None):
    K = pars['K']
    S0_1 =  pars['S0_1']
    S1_1 =  pars['S1_1']
    S0_2 =  pars['S0_2']
    S1_2 =  pars['S1_2']
    model_0 = (S0_1 + S1_1 * 10 ** (K - x[0])) / (1 + 10 ** (K - x[0]))
    model_1 = (S0_2 + S1_2 * 10 ** (K - x[1])) / (1 + 10 ** (K - x[1]))
    if data is None:
        return np.r_[model_0, model_1]
    return np.r_[data[0] - model_0, data[1] - model_1]


params2 = lmfit.Parameters()
params2.add('K', value=7.0, min=2.0, max=12.0)
params2.add('S0_1', value=df.y1[0], min=0.0)
params2.add('S0_2', value=df.y2[0], min=0.0)
params2.add('S1_1', value=df.y1.iloc[-1], min=0.0)
params2.add('S1_2', value=df.y2.iloc[-1], min=0.0)
mini2 = lmfit.Minimizer(residual2, params2, fcn_args=([df.x, df.x],), fcn_kws={'data': [df.y1, df.y2]})
res2 = mini2.minimize()
print(lmfit.fit_report(res2))

ci2, tr2 = lmfit.conf_interval(mini2, res2, sigmas=[.68, .95], trace=True)
print(lmfit.ci_report(ci2, with_offset=False, ndigits=2))
#+end_src

#+begin_src python :file ../_static/glmfit_np.r_.png
xfit = np.linspace(df.x.min(), df.x.max(), 100)
yfit0 = residual2(params2, [xfit, xfit])
yfit0 = yfit0.reshape(2, 100)
yfit = residual2(res2.params, [xfit, xfit])
yfit = yfit.reshape(2, 100)
plt.plot(df.x, df.y1, 'o', df.x, df.y2, 's', xfit, yfit[0], '-', xfit, yfit[1], '-', xfit, yfit0[0], '--', xfit, yfit0[1], '--')
plt.grid(True)
#+end_src

*** lmfit constraints aiming for generality
I believe a name convention would be more robust than relying on OrderedDict Params object.

#+begin_src python :results value
"S0_1".split("_")[0]
#+end_src

#+begin_src python
def exception_fcn_handler(func):
    def inner_function(*args, **kwargs):
        try:
            return func(*args, **kwargs)
        except TypeError:
            print(f"{func.__name__} only takes (1D) vector as argument besides lmfit.Parameters.")
    return inner_function

@exception_fcn_handler
def titration_pH(params, pH):
    p = {k.split("_")[0]: v for k, v in params.items()}
    return (p["S0"] + p["S1"] * 10 ** (p["K"] - pH)) / (1 + 10 ** (p["K"] - pH))

def residues(params, x, y, fcn):
    return y - fcn(params, x)


p1 = lmfit.Parameters()
p2 = lmfit.Parameters()
p1.add("K_1", value=7., min=2.0, max=12.0)
p2.add("K_2", value=7., min=2.0, max=12.0)
p1.add("S0_1", value=df.y1.iloc[0], min=0.0)
p2.add("S0_2", value=df.y2.iloc[0], min=0.0)
p1.add("S1_1", value=df.y1.iloc[-1], min=0.0)
p2.add("S1_2", value=df.y2.iloc[-1], min=0.0)

print(residues(p1, np.array(df.x), [1.97, 1.8, 1.7, 0.1, 0.1, .16, .01], titration_pH))

def gobjective(params, xl, yl, fcnl):
    nset = len(xl)
    res = []
    for i in range(nset):
        pi = {k: v for k, v in params.valuesdict().items() if k[-1]==f"{i+1}"}
        res = np.r_[res, residues(pi, xl[i], yl[i], fcnl[i])]
        # res = np.r_[res, yl[i] - fcnl[i](parsl[i], x[i])]
    return res

print(gobjective(p1+p2, [df.x, df.x], [df.y1, df.y2], [titration_pH, titration_pH]))
#+end_src

Here single.

#+begin_src python :file ../_static/glmfit0.png
mini = lmfit.Minimizer(residues, p1, fcn_args=(df.x, df.y1, titration_pH, ))
res= mini.minimize()

fit = titration_pH(res.params, df.x)
print(lmfit.report_fit(res))
plt.plot(df.x, df.y1, "o", df.x, fit, "--")
ci = lmfit.conf_interval(mini, res, sigmas=[1, 2])
lmfit.printfuncs.report_ci(ci)
#+end_src

Now global.

#+begin_src python :file ../_static/glmfit1.png
# %%timeit #66ms
pg = p1 + p2
pg['K_2'].expr = 'K_1'
# gmini = lmfit.Minimizer(gobjective, pg, fcn_args=([df.x[1:], df.x], [df.y1[1:], df.y2], [titration_pH, titration_pH]))
gmini = lmfit.Minimizer(gobjective, pg, fcn_args=([df.x, df.x], [df.y1, df.y2], [titration_pH, titration_pH]))
gres= gmini.minimize()
print(lmfit.fit_report(gres))

pp1 = {k: v for k, v in gres.params.valuesdict().items() if k.split("_")[1]==f"{1}"}
pp2 = {k: v for k, v in gres.params.valuesdict().items() if k.split("_")[1]==f"{2}"}
xfit = np.linspace(df.x.min(), df.x.max(), 100)
yfit1 = titration_pH(pp1, xfit)
yfit2 = titration_pH(pp2, xfit)
plt.plot(df.x, df.y1, "o", xfit, yfit1, "--")
plt.plot(df.x, df.y2, "s", xfit, yfit2, "--")
ci = lmfit.conf_interval(gmini, gres, sigmas=[1, 0.95])
print(lmfit.ci_report(ci, with_offset=False, ndigits=2))
#+end_src

To plot ci for the 5 parameters.

#+begin_src python :file ../_static/glmfit2.png
fig, axes = plt.subplots(1, 4, figsize=(24.2, 4.8), sharey=True)
cx, cy, grid = lmfit.conf_interval2d(gmini, gres, 'S0_1', 'K_1', 25, 25)
ctp = axes[0].contourf(cx, cy, grid, np.linspace(0, 1, 11))
fig.colorbar(ctp, ax=axes[0])
axes[0].set_xlabel('SA1')
axes[0].set_ylabel('pK1')
cx, cy, grid = lmfit.conf_interval2d(gmini, gres, 'S0_2', 'K_1', 25, 25)
ctp = axes[1].contourf(cx, cy, grid, np.linspace(0, 1, 11))
fig.colorbar(ctp, ax=axes[1])
axes[1].set_xlabel('SA2')
axes[1].set_ylabel('pK1')
cx, cy, grid = lmfit.conf_interval2d(gmini, gres, 'S1_1', 'K_1', 25, 25)
ctp = axes[2].contourf(cx, cy, grid, np.linspace(0, 1, 11))
fig.colorbar(ctp, ax=axes[2])
axes[2].set_xlabel('SB1')
axes[2].set_ylabel('pK1')
cx, cy, grid = lmfit.conf_interval2d(gmini, gres, 'S1_2', 'K_1', 25, 25)
ctp = axes[3].contourf(cx, cy, grid, np.linspace(0, 1, 11))
fig.colorbar(ctp, ax=axes[3])
axes[3].set_xlabel('SB2')
axes[3].set_ylabel('pK1')
#+end_src


#+begin_src python :file ../_static/glmfit3.png
plt.plot(np.r_[df.x, df.x], gres.residual, "o")
#+end_src

**** emcee                                                              :noexport:
:PROPERTIES:
:header-args: :eval never-export
:END:
#+begin_src python
gmini.params.add('__lnsigma', value=np.log(.1), min=np.log(0.001), max=np.log(2))
gresMC = lmfit.minimize(gobjective, method='emcee', steps=1800, #workers=8,
                        nan_policy='omit', burn=30, is_weighted=False, #thin=20,
                        params=gmini.params, args=([df.x, df.x], [df.y1, df.y2], [titration_pH, titration_pH]), progress=True)

#+end_src


This next block comes from: https://lmfit.github.io/lmfit-py/examples/example_emcee_Model_interface.html?highlight=emcee

#+begin_src python
emcee_kws = dict(steps=5000, burn=500, thin=20, is_weighted=False,)
emcee_params = gmini.params.copy()
emcee_params.add('__lnsigma', value=np.log(0.1), min=np.log(0.001), max=np.log(2.0))

mi = lmfit.Minimizer(gobjective, emcee_params, fcn_args=([df.x, df.x], [df.y1, df.y2], [titration_pH, titration_pH]))

res_emcee = mi.minimize(method="emcee", steps=500, burn=50, thin=20, is_weighted=False)
#+end_src


#+begin_src python
# result_emcee = model.fit(data=y, x=x, params=emcee_params, method='emcee',
#                          nan_policy='omit', fit_kws=emcee_kws)

lmfit.report_fit(res_emcee)
#+end_src


#+begin_src python
plt.plot(gresMC.acceptance_fraction, 'o')
plt.xlabel('walker')
plt.ylabel('acceptance frac')
#+end_src


#+begin_src python
import corner

tr = [v for v in gresMC.params.valuesdict().values()]
emcee_plot = corner.corner(gresMC.flatchain, labels=gresMC.var_names,
                            # truths=list(gresMC.params.valuesdict().values()))
                            truths=tr[:-1])
#+end_src


#+begin_src python
lmfit.report_fit(gresMC.params)
#+end_src


#+begin_src python
highest_prob = np.argmax(gresMC.lnprob)
hp_loc = np.unravel_index(highest_prob, gresMC.lnprob.shape)
mle_soln = gresMC.chain[hp_loc]
for i, par in enumerate(pg):
    pg[par].value = mle_soln[i]

print('\nMaximum Likelihood Estimation from emcee       ')
print('-------------------------------------------------')
print('Parameter  MLE Value   Median Value   Uncertainty')
fmt = '  {:5s}  {:11.5f} {:11.5f}   {:11.5f}'.format
for name, param in pg.items():
    print(fmt(name, param.value, gresMC.params[name].value,
              gresMC.params[name].stderr))
#+end_src


#+begin_src python
print('\nError estimates from emcee:')
print('------------------------------------------------------')
print('Parameter  -2sigma  -1sigma   median  +1sigma  +2sigma')

for name in pg.keys():
    quantiles = np.percentile(gresMC.flatchain[name],
                              [2.275, 15.865, 50, 84.135, 97.275])
    median = quantiles[2]
    err_m2 = quantiles[0] - median
    err_m1 = quantiles[1] - median
    err_p1 = quantiles[3] - median
    err_p2 = quantiles[4] - median
    fmt = '  {:5s}   {:8.4f} {:8.4f} {:8.4f} {:8.4f} {:8.4f}'.format
    print(fmt(name, err_m2, err_m1, median, err_p1, err_p2))
#+end_src

*** bootstrap con pandas

#+begin_src python :eval never-export
%%timeit
for i in range(100):
    tdf = pd.DataFrame([(j, i) for i in range(7) for j in range(2)]).sample(14, replace=True, ignore_index=False)
    df1 = df[["x", "y1"]].iloc[np.array(tdf[tdf[0]==0][1])]
    df2 = df[["x", "y2"]].iloc[np.array(tdf[tdf[0]==1][1])]
#+end_src


#+begin_src python
# %%timeit
def idx_sample(npoints):
    tidx = []
    for i in range(npoints):
        tidx.append((np.random.randint(2), np.random.randint(7)))
    idx1 = []
    idx2 = []
    for t in tidx:
        if t[0] == 0:
            idx1.append(t[1])
        elif t[0] == 1:
            idx2.append(t[1])
        else:
            raise Exception("Must never occur")
    return idx1, idx2

for i in range(100):
    idx1, idx2 = idx_sample(14)
    df1 = df[["x", "y1"]].iloc[idx1].sort_values(by="x", ascending=False).reset_index(drop=True)
    df2 = df[["x", "y2"]].iloc[idx2].sort_values(by="x", ascending=False).reset_index(drop=True)
#+end_src


#+begin_src python
# %%timeit  #5-6 s for nboot=7 now 0.4s
n_points = len(df)
nboot=199
np.random.seed(5)
best = lmfit.minimize(gobjective, pg, args=([df.x[1:], df.x], [df.y1[1:], df.y2], [titration_pH, titration_pH]))
nb = {k: [] for k in best.params.keys()}

for i in range(nboot):
    idx1, idx2 = idx_sample(13)
    df1 = df[["x", "y1"]].iloc[idx1].sort_values(by="x", ascending=False).reset_index(drop=True)
    df2 = df[["x", "y2"]].iloc[idx2].sort_values(by="x", ascending=False).reset_index(drop=True)
    # boot_idxs = np.random.randint(0, n_points, n_points)
    # df2 = df.iloc[boot_idxs]
    # df2=df2.sort_values(by="x", ascending=False).reset_index(drop=True)
    # # df2.reset_index(drop=True, inplace=True)
    # boot_idxs = np.random.randint(0, n_points, n_points)
    # df3 = df.iloc[boot_idxs]
    # # df3.reset_index(drop=True, inplace=True)
    # df3=df3.sort_values(by="x", ascending=False).reset_index(drop=True)
    try:
        out = lmfit.minimize(gobjective, best.params,
                             args=([df1.x, df2.x], [df1.y1, df2.y2], [titration_pH, titration_pH]),
                             calc_covar=False, method="leastsq", nan_policy="omit",  scale_covar=False)
        for k,v in out.params.items():
            nb[k].append(v.value)
    except:
        print(df1)
        print(df2)

# print(nb)
#+end_src


#+begin_src python :results value
np.quantile(nb["K_1"],[0.025, 0.5, 0.975])
#+end_src

#+begin_src python :file ../_static/bs_pd_f1.png
sb.kdeplot(data=nb, x="K_1", y="S1_2")
#+end_src


#+begin_src python :file ../_static/bs_pd_f2.png
# nb.drop("K_2", axis=1, inplace=True)
with sb.axes_style("darkgrid"):
    g = sb.PairGrid(pd.DataFrame(nb), diag_sharey=False, vars=["K_1", "S1_1", "S1_2"])
    g.map_upper(plt.hexbin, bins='log', gridsize=20, cmap="Blues", mincnt=2)
    g.map_lower(sb.kdeplot, cmap="viridis_r", fill=True)
    g.map_diag(sb.histplot, kde=True)
#+end_src


#+begin_src python :file ../_static/bs_pd_f3.png
sb.violinplot(data=nb, x="K_1", split=True)
#+end_src


#+begin_src python :file ../_static/bs_pd_f4.png
g = sb.jointplot(y="S1_2", x="K_1", data=nb, marker="+", s=25, marginal_kws=dict(bins=25, fill=False, kde=True), color="#2075AA", marginal_ticks=True, height=5, ratio=2)
g.plot_joint(sb.kdeplot, color="r", zorder=0, levels=5)
#+end_src


#+begin_src python :file ../_static/bs_pd_f5.png
g = sb.JointGrid(data=nb, x="K_1", y="S1_2")
g.plot_joint(sb.histplot)
g.plot_marginals(sb.boxplot)
#+end_src


#+begin_src python :file ../_static/bs_pd_f6.png
f, (ax_box, ax_hist) = plt.subplots(2, sharex=True, gridspec_kw={"height_ratios": (.25, .75)})

sb.histplot(data=nb, x="K_1", kde=True, ax=ax_hist)

sb.boxplot(x="K_1", data=nb, whis=[2.5, 97.5], ax=ax_box)
sb.stripplot(x="K_1", data=nb, color=".3", alpha=0.2, ax=ax_box)
ax_box.set(xlabel='')
f.tight_layout()
# ax = sb.violinplot(x="K_1", data=nb, inner=None, color="r")
#+end_src


#+begin_src python :file ../_static/bs_pd_f7.png
import corner

g = corner.corner(pd.DataFrame(nb)[["K_1", "S1_1", "S1_2"]], labels=list(nb.keys()))
#+end_src

*** using R

#+begin_src R :session "global"
d <- read.table("../../tests/data/A01.dat")
fit = nls(V2 ~ (SB + SA * 10 **(pK - V1))/ (1 + 10 ** (pK - V1)), start = list(SB=3e4, SA=3e5, pK=7), data=d)
summary(fit)
set.seed(4)
#+end_src

#+begin_src R :session "global"
confint(fit)
#+end_src

#+begin_src R :session "global"
fz <- function(x, SA1, SB1, SA2, SB2, pK){
  y1 <- (SB1 + SA1 * 10 **(pK - x))/ (1 + 10 ** (pK - x))
  y2 <- (SB2 + SA2 * 10 **(pK - x))/ (1 + 10 ** (pK - x))
  return(rbind(y1,y2))
}
##fitg = nls(rbind(V2, V3) ~ fz(V1, SA1, SB1, SA2, SB2, pK),         start = list(SB1=3e4, SA1=3e5, SB2=3e4, SA2=3e5, pK=7), data=d)
##fitg = nls(c(V2, V3) ~ c((SB1 + SA1 * 10 **(pK - V1))/ (1 + 10 ** (pK - V1)), (SB2 + SA2 * 10 **(pK - V1))/ (1 + 10 ** (pK - V1))),         start = list(SB1=3e4, SA1=3e5, SB2=3e4, SA2=3e5, pK=7), data=d)

#+end_src

https://stats.stackexchange.com/questions/44246/nls-curve-fitting-of-nested-shared-parameters

#+begin_src R :session "global"
n1 <- length(d$V2)
n2 <- length(d$V3)

# separate fits:
fit1 = nls(V2 ~ (SB1 + SA1 * 10 **(pK - V1))/ (1 + 10 ** (pK - V1)),
           start = list(SB1=3e4, SA1=3e5, pK=7), data=d)
fit2 = nls(V3 ~ (SB2 + SA2 * 10 **(pK - V1))/ (1 + 10 ** (pK - V1)),
           start = list(SB2=3e4, SA2=3e5, pK=7), data=d)

#set up stacked variables:
## y <- c(y1,y2); x <- c(x1,x2)
y <- c(d$V2,d$V3)

lcon1 <- rep(c(1,0), c(n1,n2))
lcon2 <- rep(c(0,1), c(n1,n2))
mcon1 <- lcon1
mcon2 <- lcon2

# combined fit with common 'c' parameter, other parameters separate
fitg = nls(y ~ mcon1*(SB1 + SA1 * 10 **(pK - V1))/ (1 + 10 ** (pK - V1)) + mcon2*(SB2 + SA2 * 10 **(pK - V1))/ (1 + 10 ** (pK - V1)),
       start = list(SB1=3e4, SA1=3e5, SB2=3e4, SA2=3e5, pK=7), data=d)

confint2(fitg)
confint2(fit1)
confint2(fit2)
#+end_src

#+begin_src R :session "global"
nlstools::confint2(fitg)
#+end_src

#+begin_src R :results graphics file :session "global" :file ../_static/gR_fit1.png
nlstools::plotfit(fit2)
#+end_src

#+begin_src R :session "global"
nlstools::overview(fitg)
#+end_src

#+begin_src R :session "global"
nlstools::test.nlsResiduals(nlstools::nlsResiduals(fitg))
#+end_src

#+begin_src R :results graphics file :session "global" :file ../_static/gR_fit2.png
plot(nlstools::nlsResiduals(fitg))
## plot(nlsResiduals(fitg))
#+end_src

#+begin_src R :results graphics file :session "global" :file ../_static/gR_fit3.png
plot(nlstools::nlsConfRegions(fit))
#+end_src

#+begin_src R :results graphics file :session "global" :file ../_static/gR_fit4.png
plot(nlstools::nlsContourRSS(fit))
#+end_src

#+begin_src R :results graphics file :session "global" :file ../_static/gR_fit5.png
library(nlstools)
nb = nlsBoot(fit, niter=999)
plot(nb)
#+end_src

#+begin_src R :results graphics file :session "global" :file ../_static/gR_fit6.png
plot(nb, type="boxplot")
#+end_src

#+begin_src R :results output :exports both :session "global"
summary(nb)
#+end_src


#+begin_src R :results graphics file :session "global" :file ../_static/gR_fit7.png
plot(nlsJack(fit))
#+end_src

#+begin_src R :session "global"
summary(nlsJack(fit))
#+end_src

*** lmfit.Model

It took 9 vs 5 ms.
It is not possible to do global fitting. In the documentation it is stressed the need to convert the output of the residue to be 1D vectors.

#+begin_src python
mod = lmfit.models.ExpressionModel("(SB + SA * 10**(pK-x)) / (1 + 10**(pK-x))")
result = mod.fit(np.array(df.y1), x=np.array(df.x), pK=7, SB=7e3, SA=10000)
print(result.fit_report())
#+end_src

#+begin_src python :file ../_static/lmodel1.png
plt.plot(df.x, df.y1, 'o')
plt.plot(df.x, result.init_fit, '--', label='initial fit')
plt.plot(df.x, result.best_fit, '-', label='best fit')
plt.legend()
#+end_src

#+begin_src python
print(result.ci_report())
#+end_src

which is faster but still I failed to find the way to global fitting.

#+begin_src python
def tit_pH(x, S0, S1, K):
    return (S0 + S1 * 10 ** (K - x)) / (1 + 10 ** (K - x))

tit_model1 = lmfit.Model(tit_pH, prefix="ds1_")
tit_model2 = lmfit.Model(tit_pH, prefix="ds2_")
print(f'parameter names: {tit_model1.param_names}')
print(f'parameter names: {tit_model2.param_names}')
print(f'independent variables: {tit_model1.independent_vars}')
print(f'independent variables: {tit_model2.independent_vars}')

tit_model1.set_param_hint('K', value=7.0, min=2.0, max=12.0)
tit_model1.set_param_hint('S0', value=df.y1[0], min=0.0)
tit_model1.set_param_hint('S1', value=df.y1.iloc[-1], min=0.0)
tit_model2.set_param_hint('K', value=7.0, min=2.0, max=12.0)
tit_model2.set_param_hint('S0', value=df.y1[0], min=0.0)
tit_model2.set_param_hint('S1', value=df.y1.iloc[-1], min=0.0)
pars1 = tit_model1.make_params()
pars2 = tit_model2.make_params()
# gmodel = tit_model1 + tit_model2
# result = gmodel.fit(df.y1 + df.y2, pars, x=df.x)
res1 = tit_model1.fit(df.y1, pars1, x=df.x)
res2 = tit_model2.fit(df.y2, pars2, x=df.x)
print(res1.fit_report())
print(res2.fit_report())
#+end_src


#+begin_src python :file ../_static/lmodel2.png
xfit_delta = (df.x.max() - df.x.min()) / 100
xfit = np.arange(df.x.min() - xfit_delta, df.x.max() + xfit_delta, xfit_delta)
dely1 = res1.eval_uncertainty(x=xfit) * 1
dely2 = res2.eval_uncertainty(x=xfit) * 1
best_fit1 = res1.eval(x=xfit)
best_fit2 = res2.eval(x=xfit)
plt.plot(df.x, df.y1, "o")
plt.plot(df.x, df.y2, "o")
plt.plot(xfit, best_fit1,"-.")
plt.plot(xfit, best_fit2,"-.")
plt.fill_between(xfit, best_fit1 - dely1, best_fit1 + dely1, color='#FEDCBA', alpha=0.5)
plt.fill_between(xfit, best_fit2 - dely2, best_fit2 + dely2, color='#FEDCBA', alpha=0.5)
#+end_src

Please mind the difference in the uncertainty between the 2 label blocks.

#+begin_src python
def tit_pH2(x, S0_1, S0_2, S1_1, S1_2, K):
    y1 = (S0_1 + S1_1 * 10 **(K - x)) / (1 + 10 **(K - x))
    y2 = (S0_2 + S1_2 * 10 **(K - x)) / (1 + 10 **(K - x))
    # return y1, y2
    return np.r_[y1, y2]

tit_model = lmfit.Model(tit_pH2)
tit_model.set_param_hint('K', value=7.0, min=2.0, max=12.0)
tit_model.set_param_hint('S0_1', value=df.y1[0], min=0.0)
tit_model.set_param_hint('S0_2', value=df.y2[0], min=0.0)
tit_model.set_param_hint('S1_1', value=df.y1.iloc[-1], min=0.0)
tit_model.set_param_hint('S1_2', value=df.y2.iloc[-1], min=0.0)
pars = tit_model.make_params()
# res = tit_model.fit([df.y1, df.y2], pars, x=df.x)
res = tit_model.fit(np.r_[df.y1, df.y2], pars, x=df.x)
print(res.fit_report())
#+end_src

#+begin_src python
dely = res.eval_uncertainty(x=xfit)
# res.plot() # this return error because of the global fit
#+end_src

#+begin_src python :file ../_static/lmodel_H04.png
def fit_pH(fp):
    df = pd.read_csv(fp)
    def tit_pH(x, SA, SB, pK):
        return (SB + SA * 10 ** (pK - x)) / (1 + 10 ** (pK - x))
    mod = lmfit.Model(tit_pH)
    pars = mod.make_params(SA=10000, SB=7e3, pK=7)
    result = mod.fit(df.y2, pars, x=df.x)
    return result, df.y2, df.x, mod

# r,y,x,model = fit_pH("/home/dati/ibf/IBF/Database/Random mutag results/Liasan-analyses/2016-05-19/2014-02-20/pH/dat/C12.dat")
r,y,x,model = fit_pH("../../tests/data/H04.dat")
xfit = np.linspace(x.min(),x.max(),50)
dely = r.eval_uncertainty(x=xfit) * 1
best_fit = r.eval(x=xfit)
plt.plot(x, y, "o")
plt.plot(xfit, best_fit,"-.")
plt.fill_between(xfit, best_fit-dely,
                 best_fit+dely, color='#FEDCBA', alpha=0.5)
r.conf_interval(sigmas=[2])
print(r.ci_report(with_offset=False, ndigits=2))
#+end_src


#+begin_src python :file ../_static/lmodel4.png
g = r.plot()
#+end_src

#+begin_src python
print(r.ci_report())
#+end_src

#+begin_src python
emcee_kws = dict(steps=2000, burn=500, thin=2, is_weighted=False,
                 progress=False)
emcee_params = r.params.copy()
emcee_params.add('__lnsigma', value=np.log(0.1), min=np.log(0.001), max=np.log(2000.0))
result_emcee = model.fit(data=y, x=x, params=emcee_params, method='emcee',
                         nan_policy='omit', fit_kws=emcee_kws)

lmfit.report_fit(result_emcee)
#+end_src

#+begin_src python :file ../_static/lmodel5.png
result_emcee.plot_fit()
#+end_src

#+begin_src python :file ../_static/lmodel6.png
emcee_corner = corner.corner(result_emcee.flatchain, labels=result_emcee.var_names,
                             truths=list(result_emcee.params.valuesdict().values()))
#+end_src

#+begin_src python
highest_prob = np.argmax(result_emcee.lnprob)
hp_loc = np.unravel_index(highest_prob, result_emcee.lnprob.shape)
mle_soln = result_emcee.chain[hp_loc]
print("\nMaximum Likelihood Estimation (MLE):")
print('----------------------------------')
for ix, param in enumerate(emcee_params):
    print(f"{param}: {mle_soln[ix]:.3f}")

quantiles = np.percentile(result_emcee.flatchain['pK'], [2.28, 15.9, 50, 84.2, 97.7])
print(f"\n\n1 sigma spread = {0.5 * (quantiles[3] - quantiles[1]):.3f}")
print(f"2 sigma spread = {0.5 * (quantiles[4] - quantiles[0]):.3f}")
#+end_src

** TODO See also this tutorial
https://www.astro.rug.nl/software/kapteyn/kmpfittutorial.html

*** TODO jackknife to auto-reject
*** TODO uncertainty estimate
* Example 2P Cl–ratio
** using lmfit.model
#+begin_src python :file ../_static/ratio2P-lmodel1.png
def fit_Rcl(fp):
    df = pd.read_table(fp)
    def R_Cl(cl, R0, R1, Kd):
        return (R1 * cl + R0 * Kd)/(Kd + cl)
    mod = lmfit.Model(R_Cl)
    pars = mod.make_params(R0=0.8, R1=0.05, Kd=10)
    result = mod.fit(df.R, pars, cl=df.cl)
    return result, df.R, df.cl, mod

r,y,x,model = fit_Rcl("../../tests/data/ratio2P.txt")
xfit = np.linspace(x.min(),x.max(),50)
dely = r.eval_uncertainty(cl=xfit) * 3
best_fit = r.eval(cl=xfit)
plt.plot(x, y, "o")
plt.grid()
plt.plot(xfit, best_fit,"-.")
plt.fill_between(xfit, best_fit-dely,
                 best_fit+dely, color='#FEDCBA', alpha=0.5)
r.conf_interval(sigmas=[2])
print(r.ci_report(with_offset=False, ndigits=2))
#+end_src

#+begin_src python
emcee_kws = dict(steps=3000, burn=300, thin=2, is_weighted=False,
                 progress=False)
emcee_params = r.params.copy()
emcee_params.add('__lnsigma', value=np.log(0.1), min=np.log(0.000001), max=np.log(2000.0))
result_emcee = model.fit(data=y, cl=x, params=emcee_params, method='emcee',
                         nan_policy='omit', fit_kws=emcee_kws)
#+end_src

#+begin_src python
lmfit.report_fit(result_emcee)
#+end_src

#+begin_src python :file ../_static/ratio2P-lmodel2.png
emcee_corner = corner.corner(result_emcee.flatchain, labels=result_emcee.var_names,
                             truths=list(result_emcee.params.valuesdict().values()))
#+end_src

#+begin_src python
highest_prob = np.argmax(result_emcee.lnprob)
hp_loc = np.unravel_index(highest_prob, result_emcee.lnprob.shape)
mle_soln = result_emcee.chain[hp_loc]
print("\nMaximum Likelihood Estimation (MLE):")
print('----------------------------------')
for ix, param in enumerate(emcee_params):
    print(f"{param}: {mle_soln[ix]:.3f}")

quantiles = np.percentile(result_emcee.flatchain['Kd'], [2.28, 15.9, 50, 84.2, 97.7])
print(f"\n\n1 sigma spread = {0.5 * (quantiles[3] - quantiles[1]):.3f}")
print(f"2 sigma spread = {0.5 * (quantiles[4] - quantiles[0]):.3f}")
#+end_src

** using R
#+begin_src  R :session "global"
d <- read.delim("../../tests/data/ratio2P.txt")
fitr = nls(R ~ (R1 * cl + R0 * Kd)/(Kd + cl), start = list(R0=0.8, R1=0.05, Kd=10), data=d)
nlstools::overview(fitr)
#+end_src

#+begin_src R :session "global"
nlstools::test.nlsResiduals(nlstools::nlsResiduals(fitr))
#+end_src

#+begin_src R :results graphics file :session "global" :file ../_static/ratio2P_R1.png
plot(nlstools::nlsResiduals(fitr))
#+end_src

#+begin_src R :results graphics file :session "global" :file ../_static/ratio2P_R2.png
plot(nlstools::nlsConfRegions(fitr))
#+end_src

#+begin_src R :results graphics file :session "global" :file ../_static/ratio2P_R3.png
plot(nlstools::nlsContourRSS(fitr))
#+end_src

#+begin_src R :results graphics file :session "global" :file ../_static/ratio2P_R4.png
library(nlstools)
set.seed(4)
nb = nlsBoot(fitr, niter=999)
plot(nb)
#+end_src

#+begin_src R :results graphics file :session "global" :file ../_static/ratio2P_R5.png
plot(nb, type="boxplot")
#+end_src

#+begin_src R :results output :exports both :session "global"
summary(nb)
#+end_src

#+begin_src R :results graphics file :session "global" :file ../_static/ratio2P_R6.png
plot(nlsJack(fitr))
#+end_src

#+begin_src R :session "global"
summary(nlsJack(fitr))
#+end_src

* Old scripts
** =fit_titration.py=

- input ← csvtable and note _file
  - csvtable
  #+begin_src ditaa :exports results :file ../_static/csvtable.png
  	+--------+-----+---+---+---+-----+
  	| lambda | A01 | . | . | . | H12 |
  	+--------+-----+---+---+---+-----+
  	|  500   |  .  | . | . | . |  .  |
  	|   .    |  .  | . | . | . |  .  |
  	|   .    |  .  | . | . | . |  .  |
  	|   .    |  .  | . | . | . |  .  |
  	|  650   |  .  | . | . | . |  .  |
  	+--------+-----+---+---+---+-----+
  #+end_src

  - note _file
  #+begin_src ditaa :exports results :file ../_static/note_file.png
  	+-----+----+----+-----+
  	|well | pH | Cl | mut |
  	+-----+----+----+-----+
  	| A01 | .  | .  |  .  |
  	|  .  | .  | .  |  .  |
  	|  .  | .  | .  |  .  |
  	| H12 | .  | .  |  .  |
  	+-----+----+----+-----+
  #+end_src

- output → pK spK and pdf of analysis


It is a unique script for pK and Cl and various methods:

    1. svd
    2. bands
    3. single lambda

and bootstraping



I do not know how to unittest
TODO

    - average spectra
    - join spectra ['B', 'E', 'F']
    - compute band integral (or sums)

** =fit_titration_global.py=

A script for fitting tuples (y1, y2) of values for each concentration (x). It uses lmfit confint and bootstrap.

- input ← x y1 y2 (file)
  - file
  #+begin_src ditaa :exports results :file ../_static/file.png
  	+------+----+----+
  	| conc | y1 | y2 |
  	+------+----+----+
  	|  .   | .  | .  |
  	|  .   | .  | .  |
  	|  .   | .  | .  |
  	+------+----+----+
  #+end_src

- output →
  - params: K SA1 SB1 SA2 SB2
  - fit.png
  - correl.png

It uses lmfit confint and bootstrap. In global fit the best approach was using lmfit without bootstrap.

#+begin_src bash :eval never
     for i in *.dat; do gfit $i png2 --boot 99 > png2/$i.txt; done
#+end_src

** IBF database uses

Bash scripts (probably moved into prtecan) for:
- =fit_titration_global.py=
  - [[../../src/clophfit/old/bash/fit.tecan]]
  - [[../../src/clophfit/old/bash/fit.tecan.cl]]
- =fit_titration.py=
  #+begin_src sh :eval never
  cd 2014-xx-xx

  (prparser) pr.enspire *.csv

  fit_titration.py meas/Copy_daniele00_893_A.csv A02_37_note.csv -d fit/37C | tee fit/svd_Copy_daniele00_893_A_A02_37_note.txt

  w_ave.sh > pKa.txt

  head pKa??/pKa.txt >> Readme.txt


  # fluorimeter data
  ls > list
  merge.py list
  fit_titration *.csv fluo_note
  #+end_src

see: [[file:/home/dati/ibf/IBF/Database/Data and protocols_Liaisan/library after Omnichange mutagenesis/Readme_howto.txt]]
