bindtools 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bindtools/__init__.py +0 -0
- bindtools/binding.py +1658 -0
- bindtools-0.1.0.dist-info/METADATA +17 -0
- bindtools-0.1.0.dist-info/RECORD +5 -0
- bindtools-0.1.0.dist-info/WHEEL +4 -0
bindtools/binding.py
ADDED
|
@@ -0,0 +1,1658 @@
|
|
|
1
|
+
# mamba create -n binding -c conda-forge python jupyter tqdm ipython uncertainties lmfit scipy numpy emcee tqdm numba corner matplotlib numdifftools
|
|
2
|
+
# test
|
|
3
|
+
|
|
4
|
+
# Import necessary modules.
|
|
5
|
+
import datetime
|
|
6
|
+
import math
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
from multiprocessing import Pool, cpu_count
|
|
10
|
+
from contextlib import nullcontext
|
|
11
|
+
|
|
12
|
+
import corner
|
|
13
|
+
import emcee
|
|
14
|
+
import h5py
|
|
15
|
+
import lmfit
|
|
16
|
+
import matplotlib.pyplot as plt
|
|
17
|
+
import numpy as np
|
|
18
|
+
import pandas as pd
|
|
19
|
+
import scipy as sp
|
|
20
|
+
from numba import jit, njit
|
|
21
|
+
from scipy.integrate import odeint
|
|
22
|
+
|
|
23
|
+
#from optimparallel import minimize_parallel # not required but can be useful if you want to do parallel LBGFS
|
|
24
|
+
#from IPython.display import display, Markdown # lets us print tables of values tables using standard iPython
|
|
25
|
+
from uncertainties import ufloat, umath
|
|
26
|
+
|
|
27
|
+
from typing import Optional,List
|
|
28
|
+
|
|
29
|
+
import logging
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
31
|
+
#import pickle as pkl
|
|
32
|
+
|
|
33
|
+
class EquilibriumError(Exception):
|
|
34
|
+
def __init__(self,message,val,params):
|
|
35
|
+
# print("Equilibrium solver failed to converge.")
|
|
36
|
+
self.val = val
|
|
37
|
+
#pkl.dump(params,open('params.pkl','wb'))
|
|
38
|
+
|
|
39
|
+
#raise ValueError("End now")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# Set everything up - all functions for fitting
|
|
43
|
+
#
|
|
44
|
+
# #https://github.com/numba/numba/issues/1269#issuecomment-702665837
|
|
45
|
+
# This solution enables us to apply the overloaded np.prod function along a single axis, see link above
|
|
46
|
+
@jit(nopython=True)
|
|
47
|
+
def apply_along_axis_0(func1d, arr):
|
|
48
|
+
"""Like calling func1d(arr, axis=0)"""
|
|
49
|
+
if arr.size == 0:
|
|
50
|
+
raise RuntimeError("Must have arr.size > 0")
|
|
51
|
+
ndim = arr.ndim
|
|
52
|
+
if ndim == 0:
|
|
53
|
+
raise RuntimeError("Must have ndim > 0")
|
|
54
|
+
elif 1 == ndim:
|
|
55
|
+
return func1d(arr)
|
|
56
|
+
else:
|
|
57
|
+
result_shape = arr.shape[1:]
|
|
58
|
+
out = np.empty(result_shape, arr.dtype)
|
|
59
|
+
_apply_along_axis_0(func1d, arr, out)
|
|
60
|
+
return out
|
|
61
|
+
|
|
62
|
+
# This solution enables us to apply the overloaded np.prod function along a single axis, see link above
|
|
63
|
+
@jit(nopython=True,nogil=True)
|
|
64
|
+
def _apply_along_axis_0(func1d, arr, out):
|
|
65
|
+
"""Like calling func1d(arr, axis=0, out=out). Require arr to be 2d or bigger."""
|
|
66
|
+
ndim = arr.ndim
|
|
67
|
+
if ndim < 2:
|
|
68
|
+
raise RuntimeError("_apply_along_axis_0 requires 2d array or bigger")
|
|
69
|
+
elif ndim == 2: # 2-dimensional case
|
|
70
|
+
for i in range(len(out)):
|
|
71
|
+
out[i] = func1d(arr[:, i])
|
|
72
|
+
else: # higher dimensional case
|
|
73
|
+
for i, out_slice in enumerate(out):
|
|
74
|
+
_apply_along_axis_0(func1d, arr[:, i], out_slice)
|
|
75
|
+
|
|
76
|
+
# This function solves the equilibrium concentration problem based on equilibrium constants, adapted
|
|
77
|
+
# from Maeder and co workers: https://doi.org/10.1016/S0922-3487(07)80006-2
|
|
78
|
+
@jit(nopython=True,nogil=True,cache=True)
|
|
79
|
+
def getConcs(eqMat,initComponentConc,logK):
|
|
80
|
+
# now we give a vector containing the (log) equilibrium constants. Be careful
|
|
81
|
+
# with sign because these constants are for formation of the complexes
|
|
82
|
+
# above from the "pure components".
|
|
83
|
+
K = 10.0**logK
|
|
84
|
+
# make an initial guess. This doesn't need to be good - it should just
|
|
85
|
+
# be nearly-equally bad for all parameters
|
|
86
|
+
# the initial guess is for the concentrations of the *free components* only
|
|
87
|
+
guessCompConc = np.zeros((1,len(initComponentConc))) + np.mean(initComponentConc)
|
|
88
|
+
eqMat = eqMat.astype('float64') # fix bug in simple systems
|
|
89
|
+
# solve the equilibrium problem above using the Newton Raphson method
|
|
90
|
+
comp,spec = DoNR(eqMat,K,initComponentConc,guessCompConc)
|
|
91
|
+
return spec
|
|
92
|
+
|
|
93
|
+
def getConcsScipy(eqMat,initComponentConc,logK,alg='L-BFGS-B'):
|
|
94
|
+
K = 10.0**logK
|
|
95
|
+
guessCompConc = np.zeros(len(initComponentConc)) + np.mean(initComponentConc)
|
|
96
|
+
|
|
97
|
+
bds = [(0,np.max(initComponentConc)) for _ in range(len(guessCompConc))]
|
|
98
|
+
# jac=True says that specObj returns float,arr(n) where arr(n) is the Jacobian
|
|
99
|
+
res=sp.optimize.minimize(specObj,guessCompConc,jac=True,args=(initComponentConc,eqMat,K),method=alg,bounds=bds,tol=0,options={'gtol': 1e-20})
|
|
100
|
+
|
|
101
|
+
compTotCalc,specConc = specCalc(res.x,len(K),eqMat,K)
|
|
102
|
+
return specConc
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@jit(nopython=True,nogil=True,cache=True)
|
|
106
|
+
def specCalc(conc,nspecies,eqMat,K): # this function calculates the equilibrium concentrations of the species
|
|
107
|
+
specmat = conc.repeat(nspecies).reshape((-1, nspecies))
|
|
108
|
+
eq3pt47 = apply_along_axis_0(np.prod,specmat**eqMat)
|
|
109
|
+
speciesConc = K * eq3pt47 # eq 3.48
|
|
110
|
+
|
|
111
|
+
compTotCalc = eqMat @ speciesConc
|
|
112
|
+
return compTotCalc,speciesConc
|
|
113
|
+
|
|
114
|
+
@jit(nopython=True,nogil=True,cache=True)
|
|
115
|
+
def specJac(conc,initComponentConc,eqMat,K): # this function calculates the Jacobian of the equilibrium problem
|
|
116
|
+
ncomp=np.shape(eqMat)[0]
|
|
117
|
+
nspecies=len(K)
|
|
118
|
+
_,speciesConc = specCalc(conc,nspecies,eqMat,K) # calculate species concentrations
|
|
119
|
+
J = _specJac(ncomp,eqMat,speciesConc)
|
|
120
|
+
return J
|
|
121
|
+
|
|
122
|
+
@jit(nopython=True,nogil=True,cache=True)
|
|
123
|
+
def _specJac(ncomp,eqMat,speciesConc): # this function calculates the Jacobian of the equilibrium problem
|
|
124
|
+
|
|
125
|
+
J = np.zeros((ncomp,ncomp))
|
|
126
|
+
# calculate Jacobian
|
|
127
|
+
for ii in range(ncomp):
|
|
128
|
+
for jj in range(ii, ncomp):
|
|
129
|
+
J[ii, jj] = np.sum(eqMat[ii, :] * eqMat[jj, :] * speciesConc)
|
|
130
|
+
if ii != jj:
|
|
131
|
+
J[jj, ii] = J[ii, jj]
|
|
132
|
+
|
|
133
|
+
return J
|
|
134
|
+
|
|
135
|
+
def specObj(conc,initComponentConc,eqMat,K):
|
|
136
|
+
nspecies = len(K)
|
|
137
|
+
compTotCalc,specConc = specCalc(conc,nspecies,eqMat,K)
|
|
138
|
+
|
|
139
|
+
# Calculate the objective function (sum of squared residuals)
|
|
140
|
+
residual = initComponentConc - compTotCalc
|
|
141
|
+
obj = np.sum(residual**2)
|
|
142
|
+
|
|
143
|
+
# Calculate the Jacobian of the objective function
|
|
144
|
+
J = _specJac(len(initComponentConc),eqMat,specConc)
|
|
145
|
+
# Jacobian of objective function: d/dc_i sum((target - calculated)^2) = -2 * sum(J_ij * (target_j - calculated_j))
|
|
146
|
+
grad = -2.0 * J @ residual
|
|
147
|
+
|
|
148
|
+
return obj, grad
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
# This function implements the Newton Raphson method for solving the equilibrium problem
|
|
152
|
+
# following: https://doi.org/10.1016/S0922-3487(07)80006-2
|
|
153
|
+
#@jit("Tuple((f8[:],f8[:]))(f8[:,:],f8[:],f8[:],f8[:])",nopython=True,nogil=True)
|
|
154
|
+
@jit(nopython=True,nogil=True,cache=True)
|
|
155
|
+
def DoNR(eqMat,K,initComponentConc,guessCompConc):
|
|
156
|
+
nspecies = len(K)
|
|
157
|
+
ncomp = len(initComponentConc)
|
|
158
|
+
|
|
159
|
+
initComponentConc[initComponentConc<=0] = 1e-20 # avoids numerical errors. Changed to <=0 rather than == 0 because sometimes small negative errors appear which makes
|
|
160
|
+
# the optimisation impossible
|
|
161
|
+
|
|
162
|
+
conc = np.copy(guessCompConc)
|
|
163
|
+
for iter in range(0,600):
|
|
164
|
+
|
|
165
|
+
compTotCalc,speciesConc = specCalc(conc,nspecies,eqMat,K)
|
|
166
|
+
|
|
167
|
+
# if our computed component concentrations are close
|
|
168
|
+
# enough to the true concentrations, we can stop
|
|
169
|
+
deltaComp = initComponentConc - compTotCalc
|
|
170
|
+
if np.all(np.abs(deltaComp) < 1e-15):
|
|
171
|
+
return compTotCalc,speciesConc
|
|
172
|
+
|
|
173
|
+
# otherwise calculate the Jacobian
|
|
174
|
+
J = _specJac(ncomp,eqMat,speciesConc)
|
|
175
|
+
|
|
176
|
+
# estimate the change in component concentrations
|
|
177
|
+
deltaConc = np.linalg.lstsq(J, deltaComp)[0].T * conc # , rcond=None
|
|
178
|
+
|
|
179
|
+
# if the change in component concentrations is too small, we are stuck
|
|
180
|
+
if(np.any(deltaConc == 0) and np.max(np.abs(deltaConc)) < 1e-15):
|
|
181
|
+
# we are not going anywhere, let's start again with a subtly different initial guess
|
|
182
|
+
conc = guessCompConc+np.random.randn(len(guessCompConc))*1e-10
|
|
183
|
+
else:
|
|
184
|
+
conc += deltaConc
|
|
185
|
+
|
|
186
|
+
if (iter+1) % 200 == 0:
|
|
187
|
+
# assume we are stuck, try new guess
|
|
188
|
+
conc = guessCompConc+np.random.randn(len(guessCompConc))*1e-10
|
|
189
|
+
|
|
190
|
+
while np.any(conc <= 0):
|
|
191
|
+
deltaConc = deltaConc/2
|
|
192
|
+
conc -= deltaConc
|
|
193
|
+
if np.all(np.abs(deltaConc)<1e-19):
|
|
194
|
+
break
|
|
195
|
+
|
|
196
|
+
raise EquilibriumError("Failed to converge in equilibrium solver doNR",speciesConc,(eqMat,K,initComponentConc,guessCompConc))
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _analytical_obs_param_token(col_name: str) -> str:
|
|
200
|
+
token = re.sub(r"[^\w]+", "_", str(col_name))
|
|
201
|
+
token = re.sub(r"_+", "_", token).strip("_")
|
|
202
|
+
if not token:
|
|
203
|
+
token = "obs"
|
|
204
|
+
if not (token[0].isalpha() or token[0] == "_"):
|
|
205
|
+
token = f"obs_{token}"
|
|
206
|
+
return token
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _best_real_root(coeffs: np.ndarray, lower: float, upper: float, score_fn) -> float:
|
|
210
|
+
nz = np.where(np.abs(coeffs) > 1e-18)[0]
|
|
211
|
+
if len(nz) == 0:
|
|
212
|
+
return float(lower)
|
|
213
|
+
|
|
214
|
+
trimmed = coeffs[nz[0]:]
|
|
215
|
+
roots = np.roots(trimmed)
|
|
216
|
+
real_roots = [float(r.real) for r in roots if abs(r.imag) < 1e-10]
|
|
217
|
+
if not real_roots:
|
|
218
|
+
return float(lower)
|
|
219
|
+
|
|
220
|
+
bounded = [r for r in real_roots if lower - 1e-12 <= r <= upper + 1e-12]
|
|
221
|
+
candidates = bounded if bounded else real_roots
|
|
222
|
+
best = min(candidates, key=score_fn)
|
|
223
|
+
return float(np.clip(best, lower, upper))
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _solve_row_11(h_tot: float, g_tot: float, beta11: float) -> tuple[float, float, float]:
|
|
227
|
+
if beta11 <= 0:
|
|
228
|
+
raise ValueError("Binding constant (in natural units) cannot be negative")
|
|
229
|
+
if h_tot < 0 or g_tot < 0:
|
|
230
|
+
raise ValueError("Total concentrations must be non-negative")
|
|
231
|
+
if h_tot ==0 or g_tot == 0:
|
|
232
|
+
return max(h_tot, 0.0), max(g_tot, 0.0), 0.0 # if either total concentration is zero, then there can be no complex
|
|
233
|
+
|
|
234
|
+
first_term = h_tot + g_tot + 1.0 / max(beta11, 1e-300)
|
|
235
|
+
hg = 0.5 * (first_term - math.sqrt(max(first_term**2 - 4.0 * h_tot * g_tot, 0.0)))
|
|
236
|
+
hg = float(np.clip(hg, 0.0, min(h_tot, g_tot))) # [HG] cannot be negative or greater than either total concentration
|
|
237
|
+
return h_tot - hg, g_tot - hg, hg
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _solve_row_12(h_tot: float, g_tot: float, beta11: float, beta12: float) -> tuple[float, float, float, float]:
|
|
241
|
+
if h_tot <= 0 or g_tot <= 0:
|
|
242
|
+
return max(h_tot, 0.0), max(g_tot, 0.0), 0.0, 0.0
|
|
243
|
+
|
|
244
|
+
coeffs = np.array(
|
|
245
|
+
[
|
|
246
|
+
beta12,
|
|
247
|
+
beta11 + 2.0 * h_tot * beta12 - g_tot * beta12,
|
|
248
|
+
1.0 + h_tot * beta11 - g_tot * beta11,
|
|
249
|
+
-g_tot,
|
|
250
|
+
],
|
|
251
|
+
dtype=float,
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
def score_fn(g_free: float) -> float:
|
|
255
|
+
d = 1.0 + beta11 * g_free + beta12 * (g_free**2)
|
|
256
|
+
if d <= 0:
|
|
257
|
+
return np.inf
|
|
258
|
+
h_free = h_tot / d
|
|
259
|
+
hg = beta11 * h_free * g_free
|
|
260
|
+
hg2 = beta12 * h_free * (g_free**2)
|
|
261
|
+
r1 = abs(h_tot - (h_free + hg + hg2))
|
|
262
|
+
r2 = abs(g_tot - (g_free + hg + 2.0 * hg2))
|
|
263
|
+
return r1 + r2
|
|
264
|
+
|
|
265
|
+
g_free = _best_real_root(coeffs, 0.0, max(g_tot, 0.0), score_fn)
|
|
266
|
+
d = max(1.0 + beta11 * g_free + beta12 * (g_free**2), 1e-300)
|
|
267
|
+
h_free = h_tot / d
|
|
268
|
+
hg = beta11 * h_free * g_free
|
|
269
|
+
hg2 = beta12 * h_free * (g_free**2)
|
|
270
|
+
return max(h_free, 0.0), max(g_free, 0.0), max(hg, 0.0), max(hg2, 0.0)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _solve_row_21(h_tot: float, g_tot: float, beta11: float, beta21: float) -> tuple[float, float, float, float]:
|
|
274
|
+
if h_tot <= 0 or g_tot <= 0:
|
|
275
|
+
return max(h_tot, 0.0), max(g_tot, 0.0), 0.0, 0.0
|
|
276
|
+
|
|
277
|
+
coeffs = np.array(
|
|
278
|
+
[
|
|
279
|
+
beta21,
|
|
280
|
+
beta11 + 2.0 * g_tot * beta21 - h_tot * beta21,
|
|
281
|
+
1.0 + g_tot * beta11 - h_tot * beta11,
|
|
282
|
+
-h_tot,
|
|
283
|
+
],
|
|
284
|
+
dtype=float,
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
def score_fn(h_free: float) -> float:
|
|
288
|
+
d = 1.0 + beta11 * h_free + beta21 * (h_free**2)
|
|
289
|
+
if d <= 0:
|
|
290
|
+
return np.inf
|
|
291
|
+
g_free = g_tot / d
|
|
292
|
+
hg = beta11 * h_free * g_free
|
|
293
|
+
h2g = beta21 * (h_free**2) * g_free
|
|
294
|
+
r1 = abs(h_tot - (h_free + hg + 2.0 * h2g))
|
|
295
|
+
r2 = abs(g_tot - (g_free + hg + h2g))
|
|
296
|
+
return r1 + r2
|
|
297
|
+
|
|
298
|
+
h_free = _best_real_root(coeffs, 0.0, max(h_tot, 0.0), score_fn)
|
|
299
|
+
d = max(1.0 + beta11 * h_free + beta21 * (h_free**2), 1e-300)
|
|
300
|
+
g_free = g_tot / d
|
|
301
|
+
hg = beta11 * h_free * g_free
|
|
302
|
+
h2g = beta21 * (h_free**2) * g_free
|
|
303
|
+
return max(h_free, 0.0), max(g_free, 0.0), max(hg, 0.0), max(h2g, 0.0)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def calc_analytical_speciation(
|
|
307
|
+
comp_concs: np.ndarray,
|
|
308
|
+
eq_mat: np.ndarray,
|
|
309
|
+
binding_params: np.ndarray,
|
|
310
|
+
topology: str,
|
|
311
|
+
n_comp: int,
|
|
312
|
+
complex_indices: list[int],
|
|
313
|
+
) -> tuple[np.ndarray, bool]:
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
n_rows = int(np.shape(comp_concs)[0])
|
|
317
|
+
n_species = int(np.shape(eq_mat)[1])
|
|
318
|
+
spec_calc = np.zeros((n_rows, n_species), dtype=float)
|
|
319
|
+
error = False
|
|
320
|
+
|
|
321
|
+
if n_comp != 2:
|
|
322
|
+
raise ValueError("Analytical fast-exchange solver requires exactly 2 components.")
|
|
323
|
+
|
|
324
|
+
beta11 = 10.0 ** float(binding_params[complex_indices[0]])
|
|
325
|
+
beta2 = None
|
|
326
|
+
if topology in ("1:2", "2:1"):
|
|
327
|
+
beta2 = 10.0 ** float(binding_params[complex_indices[1]])
|
|
328
|
+
|
|
329
|
+
for ii, row in enumerate(comp_concs):
|
|
330
|
+
h_tot = float(max(row[0], 0.0))
|
|
331
|
+
g_tot = float(max(row[1], 0.0))
|
|
332
|
+
try:
|
|
333
|
+
if topology == "1:1":
|
|
334
|
+
h_free, g_free, hg = _solve_row_11(h_tot, g_tot, beta11)
|
|
335
|
+
spec_calc[ii, 0] = h_free
|
|
336
|
+
spec_calc[ii, 1] = g_free
|
|
337
|
+
spec_calc[ii, complex_indices[0]] = hg
|
|
338
|
+
elif topology == "1:2":
|
|
339
|
+
h_free, g_free, hg, hg2 = _solve_row_12(h_tot, g_tot, beta11, float(beta2))
|
|
340
|
+
spec_calc[ii, 0] = h_free
|
|
341
|
+
spec_calc[ii, 1] = g_free
|
|
342
|
+
spec_calc[ii, complex_indices[0]] = hg
|
|
343
|
+
spec_calc[ii, complex_indices[1]] = hg2
|
|
344
|
+
elif topology == "2:1":
|
|
345
|
+
h_free, g_free, hg, h2g = _solve_row_21(h_tot, g_tot, beta11, float(beta2))
|
|
346
|
+
spec_calc[ii, 0] = h_free
|
|
347
|
+
spec_calc[ii, 1] = g_free
|
|
348
|
+
spec_calc[ii, complex_indices[0]] = hg
|
|
349
|
+
spec_calc[ii, complex_indices[1]] = h2g
|
|
350
|
+
else:
|
|
351
|
+
raise ValueError(f"Unsupported analytical topology: {topology}")
|
|
352
|
+
except Exception:
|
|
353
|
+
error = True
|
|
354
|
+
try:
|
|
355
|
+
spec_calc[ii, :] = getConcs(eq_mat, np.array([h_tot, g_tot]), binding_params)
|
|
356
|
+
except Exception:
|
|
357
|
+
spec_calc[ii, :] = 0.0
|
|
358
|
+
|
|
359
|
+
return spec_calc, error
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def _analytical_param_value(
|
|
363
|
+
values: np.ndarray, name_to_idx: dict[str, int], param_name: str
|
|
364
|
+
) -> float:
|
|
365
|
+
idx = name_to_idx.get(param_name)
|
|
366
|
+
if idx is None or idx >= len(values):
|
|
367
|
+
return 0.0
|
|
368
|
+
return float(values[idx])
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def calc_analytical_observables(
|
|
372
|
+
spec_calc: np.ndarray,
|
|
373
|
+
comp_concs: np.ndarray,
|
|
374
|
+
eq_mat: np.ndarray,
|
|
375
|
+
obs_components: list[int],
|
|
376
|
+
complex_indices: list[int],
|
|
377
|
+
shift_param_values: np.ndarray,
|
|
378
|
+
shift_param_names: list[str],
|
|
379
|
+
obs_param_map: list[list[str]],
|
|
380
|
+
) -> np.ndarray:
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
n_obs = len(obs_components)
|
|
384
|
+
out = np.zeros((spec_calc.shape[0], n_obs), dtype=float)
|
|
385
|
+
name_to_idx = {name: i for i, name in enumerate(shift_param_names)}
|
|
386
|
+
|
|
387
|
+
for obs_idx in range(n_obs):
|
|
388
|
+
comp_idx = int(obs_components[obs_idx])
|
|
389
|
+
denom = comp_concs[:, comp_idx]
|
|
390
|
+
pnames = obs_param_map[obs_idx]
|
|
391
|
+
baseline = _analytical_param_value(shift_param_values, name_to_idx, pnames[0])
|
|
392
|
+
out[:, obs_idx] = baseline
|
|
393
|
+
|
|
394
|
+
for cidx, complex_idx in enumerate(complex_indices):
|
|
395
|
+
if cidx + 1 >= len(pnames):
|
|
396
|
+
continue
|
|
397
|
+
amp = _analytical_param_value(shift_param_values, name_to_idx, pnames[cidx + 1])
|
|
398
|
+
stoich = float(eq_mat[comp_idx, complex_idx])
|
|
399
|
+
frac = np.zeros_like(denom, dtype=float)
|
|
400
|
+
np.divide(
|
|
401
|
+
stoich * spec_calc[:, complex_idx],
|
|
402
|
+
denom,
|
|
403
|
+
out=frac,
|
|
404
|
+
where=np.isfinite(denom) & (np.abs(denom) > 0),
|
|
405
|
+
)
|
|
406
|
+
out[:, obs_idx] += amp * frac
|
|
407
|
+
|
|
408
|
+
return out
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def calc_analytical_linear_observables(
|
|
412
|
+
spec_calc: np.ndarray,
|
|
413
|
+
linear_param_values: np.ndarray,
|
|
414
|
+
linear_param_names: list,
|
|
415
|
+
linear_obs_param_map: list,
|
|
416
|
+
) -> np.ndarray:
|
|
417
|
+
"""Compute concentration-weighted linear observables for the analytical fast-exchange path.
|
|
418
|
+
|
|
419
|
+
Used for UV-vis (Beer-Lambert) and fluorescence: A_i = sum_j eps_{ij} * [species_j].
|
|
420
|
+
|
|
421
|
+
Args:
|
|
422
|
+
spec_calc: species concentrations, shape (n_pts, n_species).
|
|
423
|
+
linear_param_values: flat array of current non-binding parameter values.
|
|
424
|
+
linear_param_names: list of param names corresponding to linear_param_values.
|
|
425
|
+
linear_obs_param_map: list of lists; outer index = observable column, inner index = species.
|
|
426
|
+
Each inner entry is a param name string or None (dark/silent species).
|
|
427
|
+
|
|
428
|
+
Returns:
|
|
429
|
+
ndarray of shape (n_pts, n_obs).
|
|
430
|
+
"""
|
|
431
|
+
n_obs = len(linear_obs_param_map)
|
|
432
|
+
out = np.zeros((spec_calc.shape[0], n_obs), dtype=float)
|
|
433
|
+
name_to_idx = {name: i for i, name in enumerate(linear_param_names)}
|
|
434
|
+
|
|
435
|
+
for obs_idx, pnames in enumerate(linear_obs_param_map):
|
|
436
|
+
for species_idx, pname in enumerate(pnames):
|
|
437
|
+
if not pname:
|
|
438
|
+
continue # dark / silent species
|
|
439
|
+
coeff = _analytical_param_value(linear_param_values, name_to_idx, pname)
|
|
440
|
+
out[:, obs_idx] += coeff * spec_calc[:, species_idx]
|
|
441
|
+
|
|
442
|
+
return out
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def fitfun_analytical_fast_exchange(params, fcn_opts):
|
|
446
|
+
if isinstance(params, lmfit.parameter.Parameters):
|
|
447
|
+
parvals = list(params.valuesdict().values())
|
|
448
|
+
else:
|
|
449
|
+
parvals = params
|
|
450
|
+
|
|
451
|
+
nK = int(fcn_opts["nK"])
|
|
452
|
+
binding_params = np.array([*parvals][:nK], dtype=np.float64)
|
|
453
|
+
comp_concs = np.array(fcn_opts["compConcs"], dtype=float)
|
|
454
|
+
eq_mat = np.array(fcn_opts["eqMat"], dtype=float)
|
|
455
|
+
topology = str(fcn_opts.get("analytical_topology", ""))
|
|
456
|
+
complex_indices = [int(x) for x in fcn_opts.get("analytical_complex_indices", [])]
|
|
457
|
+
obs_components = [int(x) for x in fcn_opts.get("analytical_obs_components", [])]
|
|
458
|
+
obs_param_map = list(fcn_opts.get("analytical_obs_param_map", []))
|
|
459
|
+
linear_obs_param_map = list(fcn_opts.get("analytical_linear_obs_param_map", []))
|
|
460
|
+
|
|
461
|
+
spec_calc, error = calc_analytical_speciation(
|
|
462
|
+
comp_concs=comp_concs,
|
|
463
|
+
eq_mat=eq_mat,
|
|
464
|
+
binding_params=binding_params,
|
|
465
|
+
topology=topology,
|
|
466
|
+
n_comp=int(comp_concs.shape[1]),
|
|
467
|
+
complex_indices=complex_indices,
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
if fcn_opts["optTarget"] == "concs":
|
|
471
|
+
if fcn_opts["ret"] == "residual":
|
|
472
|
+
res = (fcn_opts["exptData"] - spec_calc) / fcn_opts["sigma"]
|
|
473
|
+
if error and fcn_opts["mcmc"] is True:
|
|
474
|
+
return np.nan
|
|
475
|
+
if error:
|
|
476
|
+
res = res * 10
|
|
477
|
+
return res
|
|
478
|
+
if fcn_opts["ret"] == "concs":
|
|
479
|
+
return spec_calc
|
|
480
|
+
return -1
|
|
481
|
+
|
|
482
|
+
shift_params = np.array([*parvals][nK:], dtype=np.float64)
|
|
483
|
+
shift_param_names = fcn_opts["paramNames"][nK:]
|
|
484
|
+
|
|
485
|
+
# Route to linear (UV-vis / fluorescence) or NMR-shift observable calculation.
|
|
486
|
+
if linear_obs_param_map:
|
|
487
|
+
obs_calc = calc_analytical_linear_observables(
|
|
488
|
+
spec_calc=spec_calc,
|
|
489
|
+
linear_param_values=shift_params,
|
|
490
|
+
linear_param_names=shift_param_names,
|
|
491
|
+
linear_obs_param_map=linear_obs_param_map,
|
|
492
|
+
)
|
|
493
|
+
else:
|
|
494
|
+
obs_calc = calc_analytical_observables(
|
|
495
|
+
spec_calc=spec_calc,
|
|
496
|
+
comp_concs=comp_concs,
|
|
497
|
+
eq_mat=eq_mat,
|
|
498
|
+
obs_components=obs_components,
|
|
499
|
+
complex_indices=complex_indices,
|
|
500
|
+
shift_param_values=shift_params,
|
|
501
|
+
shift_param_names=shift_param_names,
|
|
502
|
+
obs_param_map=obs_param_map,
|
|
503
|
+
)
|
|
504
|
+
|
|
505
|
+
if fcn_opts["ret"] == "residual":
|
|
506
|
+
res = (fcn_opts["exptData"] - obs_calc) / fcn_opts["sigma"]
|
|
507
|
+
nan_mask = np.isnan(fcn_opts["exptData"])
|
|
508
|
+
res[nan_mask] = 0
|
|
509
|
+
if error and fcn_opts["mcmc"] is True:
|
|
510
|
+
return np.nan
|
|
511
|
+
if error:
|
|
512
|
+
res = res * 10
|
|
513
|
+
if fcn_opts["mcmc"] is True:
|
|
514
|
+
res = np.nan_to_num(res)
|
|
515
|
+
return res
|
|
516
|
+
if fcn_opts["ret"] == "concs":
|
|
517
|
+
return obs_calc
|
|
518
|
+
return -1
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
def fitfun(params,fcn_opts):#,eqMat,specConcs,startShifts=None,sigma=1,ret='residual'):
|
|
522
|
+
|
|
523
|
+
# fcn_opts = {'compConcs': compConcs,
|
|
524
|
+
# 'eqMat': eqMat,
|
|
525
|
+
# 'optTarget': 'obs', # or concs
|
|
526
|
+
# 'concsExpt': None,
|
|
527
|
+
# # 'obsExpt': integrals,
|
|
528
|
+
# # 'concToObs': concToObs,
|
|
529
|
+
# 'sigma': 1,
|
|
530
|
+
# 'ret': 'residual'}
|
|
531
|
+
|
|
532
|
+
if fcn_opts.get('analytical_fast_exchange') is True and fcn_opts.get('analytical_topology') in ('1:1', '1:2', '2:1'):
|
|
533
|
+
return fitfun_analytical_fast_exchange(params, fcn_opts)
|
|
534
|
+
|
|
535
|
+
if isinstance(params,lmfit.parameter.Parameters):
|
|
536
|
+
parvals = list(params.valuesdict().values())
|
|
537
|
+
else:
|
|
538
|
+
parvals = params
|
|
539
|
+
|
|
540
|
+
bindingParams = np.array([*parvals][:fcn_opts['nK']],dtype=np.float64)
|
|
541
|
+
error = False
|
|
542
|
+
specCalc = []
|
|
543
|
+
for row in fcn_opts['compConcs']:
|
|
544
|
+
try:
|
|
545
|
+
yEq = getConcs(fcn_opts['eqMat'],row,bindingParams)
|
|
546
|
+
except EquilibriumError as e:
|
|
547
|
+
yEq = e.val
|
|
548
|
+
error = True
|
|
549
|
+
specCalc.append(yEq)#yEq[4:])
|
|
550
|
+
specCalc = np.array(specCalc)
|
|
551
|
+
# except EquilibriumError as e:
|
|
552
|
+
# if fcn_opts['ret'] == 'residual':
|
|
553
|
+
# return 1000
|
|
554
|
+
# if fnc_opts
|
|
555
|
+
# specCalc = np.zeros((len(fcn_opts['compConcs']),len(e.val)))+1e-50
|
|
556
|
+
# print("Equilibrium solver failed to converge. Returning zeros.")
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
if fcn_opts['optTarget'] == 'concs':
|
|
560
|
+
if fcn_opts['ret'] == 'residual':
|
|
561
|
+
# normalize residuals
|
|
562
|
+
res= (fcn_opts['exptData']-specCalc)
|
|
563
|
+
res = res/fcn_opts['sigma']
|
|
564
|
+
if error is True: # penalty if there is an error in doNR
|
|
565
|
+
res = res*10
|
|
566
|
+
if fcn_opts['mcmc'] is True:
|
|
567
|
+
return np.nan
|
|
568
|
+
return res
|
|
569
|
+
elif fcn_opts['ret'] == 'concs':
|
|
570
|
+
return specCalc
|
|
571
|
+
|
|
572
|
+
elif fcn_opts['optTarget'] == 'obs':
|
|
573
|
+
shiftParams = np.array([*parvals][fcn_opts['nK']:],dtype=np.float64)
|
|
574
|
+
paramNames = fcn_opts['paramNames'][fcn_opts['nK']:]
|
|
575
|
+
obsCalc = concToObservable(
|
|
576
|
+
specCalc,
|
|
577
|
+
fcn_opts['specToInteg'],
|
|
578
|
+
fcn_opts['specToDd'],
|
|
579
|
+
shiftParams,
|
|
580
|
+
paramNames,
|
|
581
|
+
specToLinear=fcn_opts.get('specToLinear'),
|
|
582
|
+
)
|
|
583
|
+
|
|
584
|
+
if fcn_opts['ret'] == 'residual':
|
|
585
|
+
# normalize residuals
|
|
586
|
+
res= (fcn_opts['exptData'] - obsCalc) # function needs to know if it's working in conc or observables
|
|
587
|
+
res = res/fcn_opts['sigma']
|
|
588
|
+
|
|
589
|
+
# Set positions of nan values in exptData to 0 in res
|
|
590
|
+
nan_mask = np.isnan(fcn_opts['exptData'])
|
|
591
|
+
res[nan_mask] = 0
|
|
592
|
+
res[fcn_opts['exptData']==np.nan] = 0
|
|
593
|
+
|
|
594
|
+
if error is True: # penalty if there is an error in doNR
|
|
595
|
+
res = res*10
|
|
596
|
+
if fcn_opts['mcmc'] is True:
|
|
597
|
+
return np.nan
|
|
598
|
+
if fcn_opts['mcmc'] is True:
|
|
599
|
+
# remove nan values which tend to come from where we
|
|
600
|
+
# are unable to calculate a chemical shift due to insufficient
|
|
601
|
+
# data (i.e. zeros in specToDd)
|
|
602
|
+
if fcn_opts['specToDd'] is not None:
|
|
603
|
+
nShift = np.shape(fcn_opts['specToDd'])[1]
|
|
604
|
+
res[:,-nShift:] = np.nan_to_num(res[:,-nShift:])
|
|
605
|
+
# res[np.isnan(res)] = 1000 # set a huge residual where we have NaNs (which arise from normalizing the chemical shift)
|
|
606
|
+
# print(res)
|
|
607
|
+
return res
|
|
608
|
+
elif fcn_opts['ret'] == 'concs':
|
|
609
|
+
return obsCalc
|
|
610
|
+
|
|
611
|
+
else:
|
|
612
|
+
print("Invalid return argument. Options: 'residual' [default] or 'concs'")
|
|
613
|
+
return -1
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
def deltaToConc(deltas,mapping,startShifts,endShifts,isHost,isHG):
|
|
617
|
+
# deltas is a vector of chemical shifts
|
|
618
|
+
pass
|
|
619
|
+
|
|
620
|
+
def concToDelta(concs,specToDd,shiftParams,paramNames):
|
|
621
|
+
mlen,nlen = np.shape(specToDd)
|
|
622
|
+
#nlen = np.shape(specToDd)[1]
|
|
623
|
+
specToDdTrial = np.copy(specToDd)
|
|
624
|
+
|
|
625
|
+
# here we replace tuples in the original mapping with parameter values from the fitting engine
|
|
626
|
+
# this could be done a lot more elegantly/cleanly TODO
|
|
627
|
+
for ii in range(mlen):
|
|
628
|
+
for ij in range(nlen):
|
|
629
|
+
if isinstance(specToDdTrial[ii,ij], tuple):
|
|
630
|
+
specToDdTrial[ii,ij] = shiftParams[paramNames.index('shift_{}_{}'.format(ii,ij))]
|
|
631
|
+
|
|
632
|
+
elif isinstance(specToDdTrial[ii,ij], lmfit.Parameter):
|
|
633
|
+
specToDdTrial[ii,ij] = shiftParams[paramNames.index(specToDdTrial[ii,ij].name)]
|
|
634
|
+
specToDdTrial = specToDdTrial.astype(np.float64)
|
|
635
|
+
|
|
636
|
+
# normalize concs to mol fractions
|
|
637
|
+
truthMat = np.array(specToDdTrial,dtype=bool)
|
|
638
|
+
shiftCalc = []
|
|
639
|
+
for cc in concs:
|
|
640
|
+
tt = (truthMat.T*cc).T
|
|
641
|
+
moleFracs = tt / tt.sum(axis=0)
|
|
642
|
+
sc = (moleFracs*specToDdTrial).sum(axis=0)
|
|
643
|
+
shiftCalc.append(sc)
|
|
644
|
+
|
|
645
|
+
return shiftCalc
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
def concToLinearObs(concs: np.ndarray, specToLinear: np.ndarray, shiftParams: np.ndarray, paramNames: list) -> np.ndarray:
|
|
649
|
+
"""Compute concentration-weighted linear observables (Beer-Lambert / fluorescence).
|
|
650
|
+
|
|
651
|
+
obs_i = sum_j ( coeff_ij * [species_j] ) — no mole-fraction normalisation.
|
|
652
|
+
|
|
653
|
+
specToLinear has shape (n_species, n_obs) with dtype=object; each entry is either
|
|
654
|
+
a float (including 0.0 for dark/silent species) or an lmfit.Parameter whose current
|
|
655
|
+
value is looked up in shiftParams / paramNames.
|
|
656
|
+
|
|
657
|
+
Returns an ndarray of shape (n_pts, n_obs).
|
|
658
|
+
"""
|
|
659
|
+
mlen, nlen = np.shape(specToLinear) # (n_species, n_obs)
|
|
660
|
+
specToLinearTrial = np.copy(specToLinear)
|
|
661
|
+
|
|
662
|
+
for ii in range(mlen):
|
|
663
|
+
for ij in range(nlen):
|
|
664
|
+
if isinstance(specToLinearTrial[ii, ij], lmfit.Parameter):
|
|
665
|
+
specToLinearTrial[ii, ij] = shiftParams[paramNames.index(specToLinearTrial[ii, ij].name)]
|
|
666
|
+
specToLinearTrial = specToLinearTrial.astype(np.float64)
|
|
667
|
+
|
|
668
|
+
# Direct linear combination: (n_pts, n_species) @ (n_species, n_obs) -> (n_pts, n_obs)
|
|
669
|
+
return np.dot(concs, specToLinearTrial)
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
# This function converts a matrix of concentrations (n measurements x m species) into
|
|
673
|
+
# a matrix which permits comparison to experimental data (e.g. NMR integrals where
|
|
674
|
+
# each integral might comprise several species). The conversion is via a matrix (m species x p observables)
|
|
675
|
+
# which maps species onto integrals
|
|
676
|
+
# Scalefactor - when provided as a vector of length m - allows the observable values to be scaled
|
|
677
|
+
def concToObservable(specConcs, specToInteg, specToDd, shiftParams, paramNames, scaleFactor=None, specToLinear=None):
|
|
678
|
+
"""Convert species concentrations to predicted observables.
|
|
679
|
+
|
|
680
|
+
Observable ordering in the returned matrix: [integ cols, linear cols, shift cols].
|
|
681
|
+
The experimental data matrix must use the same column ordering.
|
|
682
|
+
"""
|
|
683
|
+
parts = []
|
|
684
|
+
|
|
685
|
+
if specToInteg is not None:
|
|
686
|
+
if scaleFactor is None:
|
|
687
|
+
scaleFactor = np.ones((np.shape(specToInteg)[1],))
|
|
688
|
+
parts.append(np.array(np.dot(specConcs, specToInteg) * scaleFactor))
|
|
689
|
+
|
|
690
|
+
if specToLinear is not None:
|
|
691
|
+
parts.append(concToLinearObs(specConcs, specToLinear, shiftParams, paramNames))
|
|
692
|
+
|
|
693
|
+
if specToDd is not None:
|
|
694
|
+
parts.append(np.array(concToDelta(specConcs, specToDd, shiftParams, paramNames)))
|
|
695
|
+
|
|
696
|
+
if len(parts) == 1:
|
|
697
|
+
return parts[0]
|
|
698
|
+
elif len(parts) > 1:
|
|
699
|
+
return np.concatenate(parts, axis=1)
|
|
700
|
+
else:
|
|
701
|
+
return np.zeros((np.shape(specConcs)[0], 0))
|
|
702
|
+
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
|
|
706
|
+
# def logprior(par):
|
|
707
|
+
# if par[2]<par[1] or par[1]<par[0]:
|
|
708
|
+
# return -np.inf
|
|
709
|
+
# if par[2]>30 or par[2]<8:
|
|
710
|
+
# return -np.inf
|
|
711
|
+
# if par[1]>20 or par[1]<3:
|
|
712
|
+
# return -np.inf
|
|
713
|
+
# if par[0]>12 or par[0]<3:
|
|
714
|
+
# return -np.inf
|
|
715
|
+
# else:
|
|
716
|
+
# return 0
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
# def fitfun_mc(params,compConcs,eqMat,specConcs):
|
|
720
|
+
# if type(params) == lmfit.parameter.Parameters:
|
|
721
|
+
# parvals = list(params.valuesdict().values())
|
|
722
|
+
# else:
|
|
723
|
+
# parvals = params
|
|
724
|
+
|
|
725
|
+
|
|
726
|
+
# params = parvals[:-1]#.valuesdict().values()
|
|
727
|
+
# lnsigma = parvals[-1]
|
|
728
|
+
# params = [*params][0:len(eqMat)]
|
|
729
|
+
# specCalc = []
|
|
730
|
+
|
|
731
|
+
|
|
732
|
+
# # if a param is getting silly, reject by checking against the priors
|
|
733
|
+
# lp = logprior(params)
|
|
734
|
+
# if np.isinf(lp):
|
|
735
|
+
# return lp
|
|
736
|
+
|
|
737
|
+
# for row in compConcs:
|
|
738
|
+
# try:
|
|
739
|
+
# yEq = getConcs(eqMat,row,np.array([0,0,0,0,*params],dtype=np.float64))
|
|
740
|
+
# except np.linalg.LinAlgError:
|
|
741
|
+
# return -np.inf
|
|
742
|
+
# # if the maths doesn't work then the solution is deemed impossible
|
|
743
|
+
# specCalc.append(yEq[3:])
|
|
744
|
+
# specCalc = np.array(specCalc)
|
|
745
|
+
|
|
746
|
+
# return (-0.5*np.sum(
|
|
747
|
+
# ((specConcs-specCalc) / np.exp(lnsigma))**2
|
|
748
|
+
# + np.log(2*np.pi) + 2*lnsigma))
|
|
749
|
+
|
|
750
|
+
|
|
751
|
+
# def diffEvolFunc(x,specConcs,compConcs):
|
|
752
|
+
# res = fitfun(compConcs,x[0],x[0]*x[1],x[0]*x[1]*x[2])
|
|
753
|
+
|
|
754
|
+
# return sum((specConcs.flatten() - res)**2)
|
|
755
|
+
|
|
756
|
+
class bindingModel():
|
|
757
|
+
def __init__(self,eqMat,compNames,speciesList,specToInteg=None,specToDd=None,colToComp=None,obsList=None,rawData=None,compConcs=None):
|
|
758
|
+
self.plist = speciesList
|
|
759
|
+
self.compNames = compNames
|
|
760
|
+
self.eqMat = eqMat
|
|
761
|
+
self._colToComp = None
|
|
762
|
+
self._compConcs = None
|
|
763
|
+
self.nConcs = None
|
|
764
|
+
self.nComp = None
|
|
765
|
+
self.obsList=obsList
|
|
766
|
+
self.comment = None
|
|
767
|
+
|
|
768
|
+
# if compConcs is not None:
|
|
769
|
+
# self.compConcs = compConcs
|
|
770
|
+
|
|
771
|
+
if colToComp is not None:
|
|
772
|
+
self.colToComp = colToComp
|
|
773
|
+
|
|
774
|
+
self._specConcs = None
|
|
775
|
+
if specToInteg is not None:
|
|
776
|
+
self.colToSpec = specToInteg
|
|
777
|
+
else:
|
|
778
|
+
self.colToSpec = None
|
|
779
|
+
|
|
780
|
+
self.rawData = rawData
|
|
781
|
+
|
|
782
|
+
if compConcs is not None:
|
|
783
|
+
self._compConcs = compConcs
|
|
784
|
+
self.nComp = np.shape(compConcs)[1]
|
|
785
|
+
self.nConcs = np.shape(compConcs)[0]
|
|
786
|
+
|
|
787
|
+
self.concUnits = None # mM #TODO
|
|
788
|
+
self.specToDd = specToDd
|
|
789
|
+
self.specToLinear: Optional[np.ndarray] = None # (n_species, n_obs) object array for UV-vis / fluorescence
|
|
790
|
+
self.analytical_fast_exchange: bool = False
|
|
791
|
+
self.analytical_topology: Optional[str] = None
|
|
792
|
+
self.analytical_obs_columns: list[str] = []
|
|
793
|
+
self.analytical_obs_components: list[int] = []
|
|
794
|
+
self.analytical_complex_indices: list[int] = []
|
|
795
|
+
self.analytical_obs_param_map: list[list[str]] = []
|
|
796
|
+
self.analytical_linear_obs_columns: list[str] = []
|
|
797
|
+
self.analytical_linear_obs_param_map: list[list] = [] # per-obs list of param names (or None) in species order
|
|
798
|
+
|
|
799
|
+
self.params = lmfit.Parameters()
|
|
800
|
+
self.mini=None
|
|
801
|
+
self.miniResult = None
|
|
802
|
+
self.fcn_opts=None
|
|
803
|
+
self.colTypes: Optional[List[ObsType]] = None
|
|
804
|
+
|
|
805
|
+
|
|
806
|
+
def _addParam(self,name,init=3,min=0,max=14,vary=True):
|
|
807
|
+
self.params[name] = lmfit.Parameter(name,init,min=min,max=max,vary=vary)
|
|
808
|
+
|
|
809
|
+
def _addExistingParam(self,param):
|
|
810
|
+
self.params[param.name] = param
|
|
811
|
+
|
|
812
|
+
@property
|
|
813
|
+
def colToComp(self):
|
|
814
|
+
return self._colToComp
|
|
815
|
+
|
|
816
|
+
@colToComp.setter
|
|
817
|
+
def colToComp(self,v):
|
|
818
|
+
self._colToComp = v
|
|
819
|
+
if v is not None:
|
|
820
|
+
self.nConcs = np.shape(v)[1]
|
|
821
|
+
self.nComp = np.shape(v)[0]
|
|
822
|
+
|
|
823
|
+
@property
|
|
824
|
+
def specConcs(self):
|
|
825
|
+
if self._specConcs is not None:
|
|
826
|
+
return self._specConcs
|
|
827
|
+
elif (self.rawData is not None) and (self.colToSpec is not None):
|
|
828
|
+
self.genSpecConcs()
|
|
829
|
+
return self._specConcs
|
|
830
|
+
else:
|
|
831
|
+
raise ValueError("Data or mapping not available, unable to generate species concentrations.")
|
|
832
|
+
|
|
833
|
+
@specConcs.setter
|
|
834
|
+
def specConcs(self,v):
|
|
835
|
+
self._specConcs = v
|
|
836
|
+
|
|
837
|
+
@property
|
|
838
|
+
def compConcs(self):
|
|
839
|
+
if self._compConcs is not None:
|
|
840
|
+
return self._compConcs
|
|
841
|
+
elif (self.rawData is not None) and (self.colToComp is not None):
|
|
842
|
+
self._compConcs = np.dot(self.rawData[:,:self.nConcs],self.colToComp.T) # [Htot, Gtot]
|
|
843
|
+
return self._compConcs
|
|
844
|
+
else:
|
|
845
|
+
raise ValueError("Data or mapping not available, unable to generate component concentrations.")
|
|
846
|
+
|
|
847
|
+
@compConcs.setter
|
|
848
|
+
def compConcs(self,v):
|
|
849
|
+
self._compConcs = v
|
|
850
|
+
|
|
851
|
+
def genSpecConcs(self,data=None,colToSpec=None):
|
|
852
|
+
if (data is None and self.rawData is None) or (self.colToSpec is None and colToSpec is None):
|
|
853
|
+
raise ValueError("No data and/or column-to-species mapping stored. Doing nothing")
|
|
854
|
+
else:
|
|
855
|
+
if data is not None:
|
|
856
|
+
self.rawData = data
|
|
857
|
+
if colToSpec is not None:
|
|
858
|
+
self.colToSpec = colToSpec
|
|
859
|
+
|
|
860
|
+
self.specConcs=np.dot(self.rawData,self.colToSpec.T)
|
|
861
|
+
|
|
862
|
+
def setColumnToSpeciesMapping(self,colToSpec):
|
|
863
|
+
self.colToSpec=colToSpec
|
|
864
|
+
|
|
865
|
+
|
|
866
|
+
def prepModel(self):
|
|
867
|
+
for paramName in self.compNames:
|
|
868
|
+
self._addParam('log'+paramName,init=0,vary=False)
|
|
869
|
+
|
|
870
|
+
for paramName in self.plist[self.nComp:]:
|
|
871
|
+
self._addParam('log'+paramName)
|
|
872
|
+
|
|
873
|
+
# Register UV-vis / fluorescence parameters from specToLinear (object array).
|
|
874
|
+
# Done before the analytical-mode branch so params are available in both paths.
|
|
875
|
+
if self.specToLinear is not None:
|
|
876
|
+
for _, x in np.ndenumerate(self.specToLinear):
|
|
877
|
+
if isinstance(x, lmfit.Parameter):
|
|
878
|
+
self._addExistingParam(x)
|
|
879
|
+
|
|
880
|
+
if self.analytical_fast_exchange:
|
|
881
|
+
n_complex = len(self.analytical_complex_indices)
|
|
882
|
+
if n_complex == 0:
|
|
883
|
+
n_complex = 1
|
|
884
|
+
self.analytical_obs_param_map = []
|
|
885
|
+
for col_name in self.analytical_obs_columns:
|
|
886
|
+
token = _analytical_obs_param_token(col_name)
|
|
887
|
+
pname_list = [f"delta0_{token}"]
|
|
888
|
+
self._addParam(pname_list[0], init=0.0, min=-1000, max=1000, vary=True)
|
|
889
|
+
for cidx in range(n_complex):
|
|
890
|
+
pname = f"deltac{cidx+1}_{token}"
|
|
891
|
+
pname_list.append(pname)
|
|
892
|
+
self._addParam(pname, init=0.0, min=-1000, max=1000, vary=True)
|
|
893
|
+
self.analytical_obs_param_map.append(pname_list)
|
|
894
|
+
# specToLinear params already registered above; no further action needed.
|
|
895
|
+
return
|
|
896
|
+
|
|
897
|
+
# add chemical shift fitting params
|
|
898
|
+
if self.specToDd is not None:
|
|
899
|
+
for ii,x in np.ndenumerate(self.specToDd):
|
|
900
|
+
if isinstance(x,tuple):
|
|
901
|
+
self._addParam('shift_{}_{}'.format(ii[0],ii[1]),x[1],min=x[0],max=x[2])
|
|
902
|
+
elif isinstance(x,lmfit.Parameter):
|
|
903
|
+
self._addExistingParam(x)
|
|
904
|
+
|
|
905
|
+
|
|
906
|
+
|
|
907
|
+
def runModel(self,sigma=1,skip_col=1,method='least_squares',ret=False,kwargs={}) -> Optional['bindingModel']:
|
|
908
|
+
|
|
909
|
+
exptData = np.copy(self.rawData)
|
|
910
|
+
spec_to_integ = None
|
|
911
|
+
if isinstance(self.colToSpec, np.ndarray) and self.colToSpec.ndim == 2 and self.colToSpec.size > 0:
|
|
912
|
+
spec_to_integ = self.colToSpec[:, skip_col:]
|
|
913
|
+
|
|
914
|
+
|
|
915
|
+
analytical_mode = bool(self.analytical_fast_exchange)
|
|
916
|
+
|
|
917
|
+
# if chemical shifts mappings not provided, then don't try to fit the chemical shifts
|
|
918
|
+
if analytical_mode:
|
|
919
|
+
exptData = exptData[:,skip_col:]
|
|
920
|
+
elif self.specToDd is None and self.specToLinear is None:
|
|
921
|
+
exptData = exptData[:,skip_col:]
|
|
922
|
+
if spec_to_integ is None:
|
|
923
|
+
raise ValueError("specToInteg mapping is required when specToDd and specToLinear are not provided.")
|
|
924
|
+
exptData = exptData[:,:np.shape(spec_to_integ)[1]]
|
|
925
|
+
else:
|
|
926
|
+
exptData = exptData[:,skip_col:]
|
|
927
|
+
|
|
928
|
+
fcn_opts = {'compConcs': self.compConcs,
|
|
929
|
+
'eqMat': self.eqMat,
|
|
930
|
+
'optTarget': 'obs', # or concs
|
|
931
|
+
#'specConcs': None,#specConcs,
|
|
932
|
+
'exptData': exptData,
|
|
933
|
+
'specToInteg': spec_to_integ,
|
|
934
|
+
'specToDd': self.specToDd,
|
|
935
|
+
'specToLinear': self.specToLinear,
|
|
936
|
+
'sigma': sigma,
|
|
937
|
+
'nK': np.shape(self.eqMat)[1],
|
|
938
|
+
'ret': 'residual',
|
|
939
|
+
'mcmc': False,
|
|
940
|
+
'paramNames': list(self.params.keys()),
|
|
941
|
+
'analytical_fast_exchange': analytical_mode,
|
|
942
|
+
'analytical_topology': self.analytical_topology,
|
|
943
|
+
'analytical_obs_columns': list(self.analytical_obs_columns),
|
|
944
|
+
'analytical_obs_components': list(self.analytical_obs_components),
|
|
945
|
+
'analytical_complex_indices': list(self.analytical_complex_indices),
|
|
946
|
+
'analytical_obs_param_map': [list(x) for x in self.analytical_obs_param_map],
|
|
947
|
+
'analytical_linear_obs_param_map': [list(x) for x in self.analytical_linear_obs_param_map],
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
#save settings for later plots
|
|
951
|
+
self.fcn_opts = fcn_opts
|
|
952
|
+
|
|
953
|
+
# do minimization, save result
|
|
954
|
+
self.mini = lmfit.Minimizer(fitfun,self.params,fcn_args=(fcn_opts,),nan_policy='omit')
|
|
955
|
+
#self.miniResult = self.mini.minimize(method='ampgo')#,verbose=1)
|
|
956
|
+
#TODO: check effect of x_scale
|
|
957
|
+
if method == 'least_squares' or method == 'leastsq':
|
|
958
|
+
# if 'method' not in kwargs:
|
|
959
|
+
# kwargs['method'] = method
|
|
960
|
+
if 'xtol' not in kwargs:
|
|
961
|
+
kwargs['xtol'] = 1e-8
|
|
962
|
+
self.miniResult = self.mini.minimize(method=method,**kwargs)#,x_scale='jac')#,verbose=1)
|
|
963
|
+
else:
|
|
964
|
+
if 'xtol' in kwargs:
|
|
965
|
+
del kwargs['xtol']
|
|
966
|
+
if 'method' not in kwargs:
|
|
967
|
+
# kwargs['method'] = method
|
|
968
|
+
pass
|
|
969
|
+
|
|
970
|
+
self.miniResult = self.mini.minimize(method=method,**kwargs)#,x_scale='jac')#,verbose=1)
|
|
971
|
+
if ret:
|
|
972
|
+
return self
|
|
973
|
+
|
|
974
|
+
|
|
975
|
+
def calcSpeciation(self,params=None):
|
|
976
|
+
"""
|
|
977
|
+
Calculate the speciation based on the current model parameters. If the optimisation has been run, it will use the fitted parameters.
|
|
978
|
+
|
|
979
|
+
Parameters:
|
|
980
|
+
- params: parameters for the model (optional)
|
|
981
|
+
|
|
982
|
+
Returns:
|
|
983
|
+
- species concentrations
|
|
984
|
+
"""
|
|
985
|
+
if params is None:
|
|
986
|
+
if self.miniResult is None:
|
|
987
|
+
params = self.params
|
|
988
|
+
else:
|
|
989
|
+
params = self.miniResult.params
|
|
990
|
+
|
|
991
|
+
fcn_opts = {'eqMat': self.eqMat,
|
|
992
|
+
'compConcs': self.compConcs,
|
|
993
|
+
'optTarget': 'concs',
|
|
994
|
+
'ret': 'concs',
|
|
995
|
+
'nK': np.shape(self.eqMat)[1],
|
|
996
|
+
'paramNames': list(params.keys()),
|
|
997
|
+
'analytical_fast_exchange': bool(self.analytical_fast_exchange),
|
|
998
|
+
'analytical_topology': self.analytical_topology,
|
|
999
|
+
'analytical_obs_columns': list(self.analytical_obs_columns),
|
|
1000
|
+
'analytical_obs_components': list(self.analytical_obs_components),
|
|
1001
|
+
'analytical_complex_indices': list(self.analytical_complex_indices),
|
|
1002
|
+
'analytical_obs_param_map': [list(x) for x in self.analytical_obs_param_map],
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
return np.array(fitfun(params, fcn_opts))
|
|
1006
|
+
|
|
1007
|
+
|
|
1008
|
+
def plotSpeciation(self,params=None,xaxisidx=None,xaxisvals=None,specToPlot=None,figname=None):
|
|
1009
|
+
"""
|
|
1010
|
+
Plot the speciation based on the current model parameters. If the optimisation has been run, it will use the fitted parameters.
|
|
1011
|
+
|
|
1012
|
+
Parameters:
|
|
1013
|
+
- params: parameters for the model (optional)
|
|
1014
|
+
- xaxisidx: which index of compConcs to use for the x-axis (default is to plot the ratio of the second/first columns (i.e. typically G/H))
|
|
1015
|
+
- xaxisvals: values for the x-axis (optional, if not provided, will use the ratio mentioned above). xaxisvals takes precedence over xaxisidx.
|
|
1016
|
+
- specToPlot: list of species to plot (optional, if not provided, will plot all species)
|
|
1017
|
+
- figname: name of the figure to save (optional, if not provided, will not save the figure)
|
|
1018
|
+
"""
|
|
1019
|
+
# If no parameters are provided, use fitted or initial parameters
|
|
1020
|
+
if params is None:
|
|
1021
|
+
if self.miniResult is None:
|
|
1022
|
+
params = self.params
|
|
1023
|
+
else:
|
|
1024
|
+
params = self.miniResult.params
|
|
1025
|
+
xx = []
|
|
1026
|
+
# Determine x-axis values for plotting
|
|
1027
|
+
if xaxisidx is None and xaxisvals is None:
|
|
1028
|
+
# Default: plot ratio of second to first component concentration
|
|
1029
|
+
xx = self.compConcs[:, 1] / self.compConcs[:, 0]
|
|
1030
|
+
elif xaxisidx is not None and xaxisvals is None:
|
|
1031
|
+
# Use specified component concentration as x-axis
|
|
1032
|
+
xx = self.compConcs[:, xaxisidx]
|
|
1033
|
+
elif xaxisvals is not None:
|
|
1034
|
+
# Use provided x-axis values
|
|
1035
|
+
xx = xaxisvals
|
|
1036
|
+
# Calculate species concentrations using current/fitted parameters
|
|
1037
|
+
specConcs = self.calcSpeciation(params)
|
|
1038
|
+
s = 5 # marker size for scatter plot
|
|
1039
|
+
plt.figure(figsize=(7, 5))
|
|
1040
|
+
# Loop over species to plot
|
|
1041
|
+
for i, species in enumerate(specToPlot if specToPlot is not None else self.plist):
|
|
1042
|
+
if specToPlot is not None:
|
|
1043
|
+
# Only plot species specified in specToPlot
|
|
1044
|
+
if species in self.plist:
|
|
1045
|
+
plt.scatter(xx, specConcs[:, self.plist.index(species)], label=f'[{species}]$_\\text{{free}}$', s=s)
|
|
1046
|
+
else:
|
|
1047
|
+
print(f"Species '{species}' not found in the model. Skipping.")
|
|
1048
|
+
else:
|
|
1049
|
+
# Plot all species
|
|
1050
|
+
plt.scatter(xx, specConcs[:, i], label=f'[{self.plist[i]}]$_\\text{{free}}$', s=s)
|
|
1051
|
+
|
|
1052
|
+
# Set x-axis label depending on what is plotted
|
|
1053
|
+
if xaxisidx is None and xaxisvals is None:
|
|
1054
|
+
plt.xlabel('Component Concentration Ratio ([G]/[H])$_\\text{tot}$')
|
|
1055
|
+
else:
|
|
1056
|
+
plt.xlabel('Component Concentration (M)' if xaxisidx is None else f'[{self.compNames[xaxisidx]}]$_\\text{{tot}}$ (M)')
|
|
1057
|
+
plt.ylabel('Free species Concentration (M)')
|
|
1058
|
+
plt.legend()
|
|
1059
|
+
plt.title('Speciation')
|
|
1060
|
+
plt.show()
|
|
1061
|
+
# Save figure if filename is provided
|
|
1062
|
+
if figname is not None:
|
|
1063
|
+
plt.savefig(figname + '.pdf', bbox_inches="tight")
|
|
1064
|
+
plt.savefig(figname + '.png', dpi=1200, bbox_inches="tight")
|
|
1065
|
+
|
|
1066
|
+
return plt.gcf()
|
|
1067
|
+
|
|
1068
|
+
|
|
1069
|
+
def simulateModel(model,compConcs=None,params=None):
|
|
1070
|
+
"""
|
|
1071
|
+
Simulate the model with given component concentrations and parameters.
|
|
1072
|
+
|
|
1073
|
+
Parameters:
|
|
1074
|
+
- model: bindingModel instance
|
|
1075
|
+
- compConcs: component concentrations (optional)
|
|
1076
|
+
- params: parameters for the model (optional)
|
|
1077
|
+
|
|
1078
|
+
Returns:
|
|
1079
|
+
- simulated concentrations of species
|
|
1080
|
+
"""
|
|
1081
|
+
if compConcs is None:
|
|
1082
|
+
compConcs = model.compConcs
|
|
1083
|
+
if params is None:
|
|
1084
|
+
params = model.miniResult.params
|
|
1085
|
+
|
|
1086
|
+
fcn_opts = model.fcn_opts.copy()
|
|
1087
|
+
fcn_opts['optTarget'] = 'obs'
|
|
1088
|
+
fcn_opts['ret'] = 'concs'
|
|
1089
|
+
fcn_opts['compConcs'] = compConcs
|
|
1090
|
+
|
|
1091
|
+
return np.array(fitfun(params, fcn_opts))
|
|
1092
|
+
|
|
1093
|
+
def getCalcData(model,newConcs=None):
|
|
1094
|
+
# Calculate calcData using fitfun
|
|
1095
|
+
fcn_opts = model.fcn_opts.copy()
|
|
1096
|
+
fcn_opts['optTarget'] = 'obs'
|
|
1097
|
+
fcn_opts['ret'] = 'concs'
|
|
1098
|
+
if newConcs is not None:
|
|
1099
|
+
fcn_opts['compConcs'] = newConcs
|
|
1100
|
+
calcData = np.array(fitfun(model.miniResult.params, fcn_opts))
|
|
1101
|
+
return calcData
|
|
1102
|
+
|
|
1103
|
+
def makeFitResidPlot(model,plotMask=None,skip_start=0,skip_end=None,figname=None,xindex=1,xvals=None,xlabel=None,ylabel='Conc. (M)',labels=None):
|
|
1104
|
+
calcData = getCalcData(model)
|
|
1105
|
+
compConcs = model.compConcs.copy()
|
|
1106
|
+
exptData = model.fcn_opts['exptData'].copy()
|
|
1107
|
+
if labels is None:
|
|
1108
|
+
labels = model.obsList
|
|
1109
|
+
# Optionally skip start and end datapoints
|
|
1110
|
+
if skip_end is None:
|
|
1111
|
+
compConcs = compConcs[skip_start:,:]
|
|
1112
|
+
exptData = exptData[skip_start:,:]
|
|
1113
|
+
calcData = calcData[skip_start:,:]
|
|
1114
|
+
else:
|
|
1115
|
+
compConcs = compConcs[skip_start:-skip_end,:]
|
|
1116
|
+
exptData = exptData[skip_start:-skip_end,:]
|
|
1117
|
+
calcData = calcData[skip_start:-skip_end,:]
|
|
1118
|
+
|
|
1119
|
+
if plotMask is not None:
|
|
1120
|
+
|
|
1121
|
+
calcData= calcData[:,plotMask]
|
|
1122
|
+
exptData = exptData[:,plotMask]
|
|
1123
|
+
|
|
1124
|
+
if xvals is None:
|
|
1125
|
+
xvals = compConcs[:,xindex]
|
|
1126
|
+
|
|
1127
|
+
if xlabel is None:
|
|
1128
|
+
xlabel='[Guest]$_{tot}$ (M)'
|
|
1129
|
+
|
|
1130
|
+
plt.gcf().clear()
|
|
1131
|
+
sf=1 # fig scale factor from single col
|
|
1132
|
+
ms=10 #marker size
|
|
1133
|
+
fig=plt.subplots(1,2,figsize=(sf*150/22.5,sf*70/22.5))
|
|
1134
|
+
#plt.figure(figsize=(sf*85/22.5,sf*70/22.5))
|
|
1135
|
+
# plt.scatter(compConcs[:,2],(calcSpec[:,3]-specConcs[:,0])/specConcs[:,0])
|
|
1136
|
+
# plt.scatter(compConcs[:,2],(calcSpec[:,4]-specConcs[:,1])/specConcs[:,1])
|
|
1137
|
+
# plt.scatter(compConcs[:,2],(calcSpec[:,5]-specConcs[:,2])/specConcs[:,2])
|
|
1138
|
+
|
|
1139
|
+
colours = ['r','k','b']
|
|
1140
|
+
points = ['^','s','o']
|
|
1141
|
+
plt.subplot(122)
|
|
1142
|
+
for ii in range(0,len(exptData[0])):
|
|
1143
|
+
plt.scatter(xvals,calcData[:,ii]-exptData[:,ii],label=labels[ii],s=ms)
|
|
1144
|
+
plt.legend()
|
|
1145
|
+
|
|
1146
|
+
# plt.scatter(10e3*compConcs[:,2],10e6*(specConcs[:,0]-specCalc[:,0]),marker=points[0],c=colours[0],label="ZnNc${\cdot}$pyridine",s=ms)
|
|
1147
|
+
# plt.scatter(10e3*compConcs[:,2],10e6*(specConcs[:,1]-specCalc[:,1]),marker=points[1],c=colours[1],label="ZnNc${\cdot}$DABCO",s=ms)
|
|
1148
|
+
# plt.scatter(10e3*compConcs[:,2],10e6*(specConcs[:,2]-specCalc[:,2]),marker=points[2],c=colours[2],label="ZnNc$_{2}{\cdot}$DABCO",s=ms)
|
|
1149
|
+
|
|
1150
|
+
|
|
1151
|
+
plt.legend()
|
|
1152
|
+
plt.xlabel(xlabel)
|
|
1153
|
+
|
|
1154
|
+
plt.ylabel("residuals")
|
|
1155
|
+
# if figname is not None:
|
|
1156
|
+
# plt.savefig(figname+'-residuals.pdf', bbox_inches="tight")
|
|
1157
|
+
# plt.savefig(figname+'-residuals.png',dpi=1200, bbox_inches="tight")
|
|
1158
|
+
|
|
1159
|
+
# plt.show()
|
|
1160
|
+
|
|
1161
|
+
# plt.figure(figsize=(sf*85/22.5,sf*70/22.5))
|
|
1162
|
+
plt.subplot(121)
|
|
1163
|
+
for ii in range(0,len(exptData[0])):
|
|
1164
|
+
plt.scatter(xvals,exptData[:,ii],s=ms)
|
|
1165
|
+
plt.plot(xvals,calcData[:,ii],label=labels[ii])
|
|
1166
|
+
#plt.xlim(0,0.07)
|
|
1167
|
+
#plt.legend()
|
|
1168
|
+
plt.xlabel(xlabel)
|
|
1169
|
+
plt.ylabel(ylabel)
|
|
1170
|
+
|
|
1171
|
+
|
|
1172
|
+
plt.tight_layout()
|
|
1173
|
+
if figname is not None:
|
|
1174
|
+
plt.savefig(figname+'.pdf', bbox_inches="tight")
|
|
1175
|
+
plt.savefig(figname+'.png',dpi=1200, bbox_inches="tight")
|
|
1176
|
+
plt.show()
|
|
1177
|
+
|
|
1178
|
+
|
|
1179
|
+
def saveFitCSV(m,mask,filename,xindex=1,xvals=None,xlabel='[G]_tot (M)',labels=None):
|
|
1180
|
+
calcData = getCalcData(m)
|
|
1181
|
+
compConcs = m.compConcs.copy()
|
|
1182
|
+
exptData = m.fcn_opts['exptData'].copy()
|
|
1183
|
+
|
|
1184
|
+
if labels is None:
|
|
1185
|
+
labels = [m.obsList[ii] for ii in mask]
|
|
1186
|
+
|
|
1187
|
+
|
|
1188
|
+
if mask is not None:
|
|
1189
|
+
calcData= calcData[:,mask]
|
|
1190
|
+
exptData = exptData[:,mask]
|
|
1191
|
+
|
|
1192
|
+
if xvals is None:
|
|
1193
|
+
xvals = compConcs[:,xindex]
|
|
1194
|
+
|
|
1195
|
+
|
|
1196
|
+
|
|
1197
|
+
ydata = np.concatenate((exptData,calcData),axis=1)
|
|
1198
|
+
data = np.concatenate((xvals[:,np.newaxis],ydata),axis=1)
|
|
1199
|
+
|
|
1200
|
+
cols = [xlabel,*['expt '+x for x in labels],*['calc '+x for x in labels]]
|
|
1201
|
+
print(cols)
|
|
1202
|
+
dataExport = pd.DataFrame(data,columns=cols)
|
|
1203
|
+
|
|
1204
|
+
|
|
1205
|
+
dataExport.to_csv(filename,index=False)
|
|
1206
|
+
|
|
1207
|
+
@jit(nopython=True,nogil=True,cache=True)
|
|
1208
|
+
def calclnprob(r,lnsigma):
|
|
1209
|
+
r= (-0.5*np.sum(
|
|
1210
|
+
np.divide(r, np.exp(lnsigma))**2
|
|
1211
|
+
+ np.log(2*np.pi) + 2*lnsigma))
|
|
1212
|
+
|
|
1213
|
+
if math.isnan(r):
|
|
1214
|
+
print('Probability function returned NaN, fudging to -inf.')
|
|
1215
|
+
return -np.inf
|
|
1216
|
+
return r
|
|
1217
|
+
|
|
1218
|
+
|
|
1219
|
+
|
|
1220
|
+
def log_prob(params,fcn_opts,bounds):
|
|
1221
|
+
#def log_prob(params):
|
|
1222
|
+
lp = log_prior(params,bounds)
|
|
1223
|
+
|
|
1224
|
+
if not np.isfinite(lp):
|
|
1225
|
+
return -np.inf
|
|
1226
|
+
|
|
1227
|
+
mapping = fcn_opts['sigmaMapping']
|
|
1228
|
+
lnsigmaVals = params[-(max(mapping)+1):]
|
|
1229
|
+
# make lnsigma vector using simparams
|
|
1230
|
+
lnsigma = np.array([lnsigmaVals[ii] for ii in mapping])
|
|
1231
|
+
|
|
1232
|
+
pp = np.zeros(len(params)+ np.shape(fcn_opts['compConcs'])[1]) # need extra parameters - for logK=0 - for each component in the eqMat
|
|
1233
|
+
|
|
1234
|
+
|
|
1235
|
+
pp[-len(params):] = params
|
|
1236
|
+
|
|
1237
|
+
fcn_opts['ret']='residual'
|
|
1238
|
+
r = np.array(fitfun(pp, fcn_opts))
|
|
1239
|
+
if np.isnan(r).any():
|
|
1240
|
+
return -np.inf
|
|
1241
|
+
|
|
1242
|
+
#r = -0.5 * ((np.sum(r**2) / np.exp(lnsigma) ) + np.log(2*np.pi) + 2*lnsigma)
|
|
1243
|
+
|
|
1244
|
+
rout = calclnprob(r,lnsigma)
|
|
1245
|
+
|
|
1246
|
+
|
|
1247
|
+
|
|
1248
|
+
# r = r/np.exp(params[-1])
|
|
1249
|
+
return rout
|
|
1250
|
+
|
|
1251
|
+
@jit(nopython=True,nogil=True,cache=True)
|
|
1252
|
+
def log_prior(val,bounds):
|
|
1253
|
+
# if val[-1]<-20 or val[-1] > -2:
|
|
1254
|
+
# return -np.inf
|
|
1255
|
+
for ii in range(len(val)):
|
|
1256
|
+
if not ( bounds[ii][0] < val[ii] < bounds[ii][1] ):
|
|
1257
|
+
return -np.inf
|
|
1258
|
+
return 0.0
|
|
1259
|
+
|
|
1260
|
+
class ObsType():
|
|
1261
|
+
def __init__(self,name,units=None,value=None,minlim=None,maxlim=None):
|
|
1262
|
+
self.name = name
|
|
1263
|
+
|
|
1264
|
+
# TODO use pint for units
|
|
1265
|
+
|
|
1266
|
+
|
|
1267
|
+
|
|
1268
|
+
if name == 'NMRInteg':
|
|
1269
|
+
if units is None:
|
|
1270
|
+
self.units = 'M'
|
|
1271
|
+
else:
|
|
1272
|
+
self.units = units
|
|
1273
|
+
|
|
1274
|
+
self.param = lmfit.Parameter('lnsigmaNMRInteg',
|
|
1275
|
+
value=-8,vary=False,
|
|
1276
|
+
min=-11,max=-5)
|
|
1277
|
+
|
|
1278
|
+
elif name == 'concMeas':
|
|
1279
|
+
if units is None:
|
|
1280
|
+
self.units = 'M'
|
|
1281
|
+
else:
|
|
1282
|
+
self.units = units
|
|
1283
|
+
|
|
1284
|
+
self.param = lmfit.Parameter('lnsigmaconcMeas',
|
|
1285
|
+
value=-8,vary=False,
|
|
1286
|
+
min=-11,max=-5)
|
|
1287
|
+
|
|
1288
|
+
elif name == 'deltaH':
|
|
1289
|
+
self.units = 'ppm'
|
|
1290
|
+
|
|
1291
|
+
self.param = lmfit.Parameter('lnsigmadeltaH',
|
|
1292
|
+
value=-9,vary=False,
|
|
1293
|
+
min=-13,max=-5)
|
|
1294
|
+
|
|
1295
|
+
|
|
1296
|
+
elif name == 'deltaF':
|
|
1297
|
+
self.units = 'ppm'
|
|
1298
|
+
|
|
1299
|
+
self.param = lmfit.Parameter('lnsigmadeltaF',
|
|
1300
|
+
value=-5,vary=False,
|
|
1301
|
+
min=-8,max=-3)
|
|
1302
|
+
|
|
1303
|
+
elif name == 'absorbance' or name =='uvvis':
|
|
1304
|
+
self.units = units if units is not None else 'absorbance'
|
|
1305
|
+
self.param = lmfit.Parameter('lnsigmaUVvis',
|
|
1306
|
+
value=-7, vary=True,
|
|
1307
|
+
min=-11, max=-3)
|
|
1308
|
+
|
|
1309
|
+
elif name == 'fluorescence':
|
|
1310
|
+
self.units = units if units is not None else 'intensity'
|
|
1311
|
+
self.param = lmfit.Parameter('lnsigmaFluorescence',
|
|
1312
|
+
value=-4, vary=True,
|
|
1313
|
+
min=-8, max=0)
|
|
1314
|
+
|
|
1315
|
+
else:
|
|
1316
|
+
self.units = units
|
|
1317
|
+
self.param = lmfit.Parameter('lnsigma'+name)
|
|
1318
|
+
|
|
1319
|
+
|
|
1320
|
+
if value is not None:
|
|
1321
|
+
self.param.value = value
|
|
1322
|
+
|
|
1323
|
+
if minlim is not None:
|
|
1324
|
+
self.param.min = minlim
|
|
1325
|
+
|
|
1326
|
+
if maxlim is not None:
|
|
1327
|
+
self.param.max = maxlim
|
|
1328
|
+
|
|
1329
|
+
self.lnsigma = self.param.value
|
|
1330
|
+
self.sigma = np.exp(self.lnsigma)
|
|
1331
|
+
self.minlim = self.param.min
|
|
1332
|
+
self.maxlim = self.param.max
|
|
1333
|
+
|
|
1334
|
+
if self.lnsigma > self.maxlim or self.lnsigma < self.minlim:
|
|
1335
|
+
print("lnsigma value out of bounds. Setting to midpoint between limits.")
|
|
1336
|
+
self.lnsigma = (self.maxlim+self.minlim)/2
|
|
1337
|
+
self.param.value = self.lnsigma
|
|
1338
|
+
print("New starting value: ",self.lnsigma)
|
|
1339
|
+
|
|
1340
|
+
|
|
1341
|
+
# convert a list of colNames (string) to a list of indices corresponding to
|
|
1342
|
+
# unique sigmas (ints)
|
|
1343
|
+
def sigmaMapping(colNames):
|
|
1344
|
+
sc=list(dict.fromkeys(colNames)) # get ordered unique list members
|
|
1345
|
+
ix = []
|
|
1346
|
+
for nn in colNames:
|
|
1347
|
+
ix.append(sc.index(nn))
|
|
1348
|
+
return ix
|
|
1349
|
+
|
|
1350
|
+
|
|
1351
|
+
class MCMC:
|
|
1352
|
+
def __init__(self, model: bindingModel, obs: List[ObsType], walkers: int =25, samples: int =5000, variance: float=0.2) -> None:
|
|
1353
|
+
self.model = model
|
|
1354
|
+
self.obs = obs
|
|
1355
|
+
self.walkers = walkers
|
|
1356
|
+
self.samples = samples
|
|
1357
|
+
self.variance = variance
|
|
1358
|
+
self.sampler = None
|
|
1359
|
+
self._labels = None
|
|
1360
|
+
|
|
1361
|
+
@property
|
|
1362
|
+
def labels(self):
|
|
1363
|
+
if self._labels is not None:
|
|
1364
|
+
return self._labels
|
|
1365
|
+
else:
|
|
1366
|
+
self._labels = self.make_labels()
|
|
1367
|
+
return self._labels
|
|
1368
|
+
|
|
1369
|
+
def make_labels(self):
|
|
1370
|
+
params = [self.model.params[pp].name for pp in self.model.params if self.model.params[pp].vary]
|
|
1371
|
+
ss = [pp.name for pp in self.obs]
|
|
1372
|
+
ss = list(dict.fromkeys(ss))
|
|
1373
|
+
ss = ['lnsigma' + x for x in ss]
|
|
1374
|
+
params += ss
|
|
1375
|
+
return params
|
|
1376
|
+
|
|
1377
|
+
|
|
1378
|
+
def run(self,ret=False,thin=1,samples=None,pool=None,tqdm_kwargs=None):
|
|
1379
|
+
bm = self.model
|
|
1380
|
+
bm.colTypes = self.obs
|
|
1381
|
+
|
|
1382
|
+
fcn_opts = bm.fcn_opts or {}
|
|
1383
|
+
bm.fcn_opts = fcn_opts
|
|
1384
|
+
|
|
1385
|
+
fcn_opts['sigma'] = 1
|
|
1386
|
+
fcn_opts['ret'] = 'residual'
|
|
1387
|
+
|
|
1388
|
+
colNames = []
|
|
1389
|
+
|
|
1390
|
+
sigmaParams = lmfit.Parameters()
|
|
1391
|
+
for pp in bm.colTypes:
|
|
1392
|
+
sigmaParams[pp.name] = pp.param
|
|
1393
|
+
colNames.append(pp.name)
|
|
1394
|
+
|
|
1395
|
+
fcn_opts['sigmaMapping'] = sigmaMapping(colNames)
|
|
1396
|
+
fcn_opts['mcmc'] = True
|
|
1397
|
+
|
|
1398
|
+
bounds = []
|
|
1399
|
+
optResult = []
|
|
1400
|
+
for pp in bm.miniResult.params.keys():
|
|
1401
|
+
if bm.miniResult.params[pp].vary:
|
|
1402
|
+
bounds.append([bm.params[pp].min, bm.params[pp].max])
|
|
1403
|
+
optResult.append(bm.miniResult.params[pp].value)
|
|
1404
|
+
|
|
1405
|
+
for pp in sigmaParams.keys():
|
|
1406
|
+
bounds.append([sigmaParams[pp].min, sigmaParams[pp].max])
|
|
1407
|
+
optResult.append(sigmaParams[pp].value)
|
|
1408
|
+
|
|
1409
|
+
override_bounds = fcn_opts.get("mcmc_bounds")
|
|
1410
|
+
if override_bounds is not None:
|
|
1411
|
+
bounds = np.array(override_bounds, dtype=float)
|
|
1412
|
+
else:
|
|
1413
|
+
bounds = np.array(bounds)
|
|
1414
|
+
ndim = len(bounds)
|
|
1415
|
+
p0 = [np.array(optResult) * (self.variance * np.random.randn(ndim) / 100 + 1) for _ in range(self.walkers)]
|
|
1416
|
+
|
|
1417
|
+
for i in range(self.walkers):
|
|
1418
|
+
if (p0[i] < bounds[:, 0]).any() or (p0[i] > bounds[:, 1]).any():
|
|
1419
|
+
logger.info("Walker initialized out of bounds; set to bound.")
|
|
1420
|
+
p0[i] = np.clip(p0[i], bounds[:, 0] + 1e-7, bounds[:, 1] - 1e-7)
|
|
1421
|
+
if samples is None:
|
|
1422
|
+
samples = self.samples
|
|
1423
|
+
|
|
1424
|
+
if self.sampler is not None:
|
|
1425
|
+
# run from previous state
|
|
1426
|
+
p0 = None
|
|
1427
|
+
self.samples += samples
|
|
1428
|
+
else:
|
|
1429
|
+
# if new samples number is being given in this function,
|
|
1430
|
+
# overwrite the object's samples record
|
|
1431
|
+
self.samples = samples
|
|
1432
|
+
|
|
1433
|
+
if os.name == 'posix' or pool is not None:
|
|
1434
|
+
# running on linux
|
|
1435
|
+
logger.debug("Running on linux. Trying to use pool/multiprocessing.")
|
|
1436
|
+
p = Pool() if pool is None else nullcontext(pool)
|
|
1437
|
+
with p as active_pool:
|
|
1438
|
+
if self.sampler is None:
|
|
1439
|
+
self.sampler = emcee.EnsembleSampler(self.walkers, ndim, log_prob, args=[bm.fcn_opts, bounds], pool=active_pool)
|
|
1440
|
+
else:
|
|
1441
|
+
self.sampler.pool = active_pool
|
|
1442
|
+
self.sampler.run_mcmc(p0, samples, progress=True,progress_kwargs=tqdm_kwargs)
|
|
1443
|
+
else:
|
|
1444
|
+
if self.sampler is None:
|
|
1445
|
+
self.sampler = emcee.EnsembleSampler(self.walkers, ndim, log_prob, args=[bm.fcn_opts, bounds])
|
|
1446
|
+
|
|
1447
|
+
self.sampler.run_mcmc(p0, samples, progress=True,thin_by=thin)#,skip_initial_state_check=True)
|
|
1448
|
+
|
|
1449
|
+
if ret is True:
|
|
1450
|
+
return self.sampler, bm
|
|
1451
|
+
|
|
1452
|
+
def plot_chain(self,title=None,fig=None):
|
|
1453
|
+
ndim = self.sampler.ndim
|
|
1454
|
+
if fig is None:
|
|
1455
|
+
fig, axes = plt.subplots(ndim, figsize=(10, 7), sharex=True)
|
|
1456
|
+
else:
|
|
1457
|
+
axes = fig.subplots(nrows=ndim,ncols=1,sharex=True)
|
|
1458
|
+
samples = self.sampler.get_chain()
|
|
1459
|
+
for i in range(ndim):
|
|
1460
|
+
ax = axes[i]
|
|
1461
|
+
ax.plot(samples[:, :, i], "k", alpha=0.3)
|
|
1462
|
+
ax.set_xlim(0, len(samples))
|
|
1463
|
+
ax.set_ylabel(self.labels[i])
|
|
1464
|
+
ax.yaxis.set_label_coords(-0.1, 0.5)
|
|
1465
|
+
axes[-1].set_xlabel("step number")
|
|
1466
|
+
if title is not None:
|
|
1467
|
+
fig.suptitle(title)
|
|
1468
|
+
|
|
1469
|
+
def plot_corner(self,title=None,burnin=None,corner_kwargs={},fig=None):
|
|
1470
|
+
try:
|
|
1471
|
+
tau = self.sampler.get_autocorr_time()
|
|
1472
|
+
except emcee.autocorr.AutocorrError as e:
|
|
1473
|
+
m = "Warning: Autocorrelation time is likely too short. Check the chain."
|
|
1474
|
+
print(m)
|
|
1475
|
+
logger.warning(m)
|
|
1476
|
+
tau = e.tau
|
|
1477
|
+
if burnin is None:
|
|
1478
|
+
burnin = int(5 * np.max(tau))
|
|
1479
|
+
logger.info("Burnin set to ",burnin)
|
|
1480
|
+
|
|
1481
|
+
f= self.make_corner_fig(title=title,burnin=burnin,corner_kwargs=corner_kwargs,fig=fig)
|
|
1482
|
+
return f
|
|
1483
|
+
|
|
1484
|
+
def make_corner_fig(self,title=None,burnin=None,corner_kwargs={},fig=None):
|
|
1485
|
+
samples = self.sampler.get_chain(discard=burnin, flat=True)
|
|
1486
|
+
if fig is None:
|
|
1487
|
+
fig=plt.figure()
|
|
1488
|
+
if title is not None:
|
|
1489
|
+
fig.suptitle(title)
|
|
1490
|
+
fig=corner.corner(samples, labels=self.labels, show_titles=True,fig=fig,**corner_kwargs)
|
|
1491
|
+
return fig
|
|
1492
|
+
else:
|
|
1493
|
+
fig=corner.corner(samples, labels=self.labels, show_titles=True,fig=fig,**corner_kwargs)
|
|
1494
|
+
return fig
|
|
1495
|
+
|
|
1496
|
+
|
|
1497
|
+
|
|
1498
|
+
|
|
1499
|
+
def get_tau(self):
|
|
1500
|
+
try:
|
|
1501
|
+
tau = self.sampler.get_autocorr_time()
|
|
1502
|
+
print("{:<20s} {:<10s}".format("Param","Tau (steps)"))
|
|
1503
|
+
print('\n'.join(["{:<20s} {:<10f}".format(*x) for x in zip(self.labels,tau)]))
|
|
1504
|
+
|
|
1505
|
+
except emcee.autocorr.AutocorrError as e:
|
|
1506
|
+
#print("Warning: Autocorrelation time is likely too short. Check the chain.")
|
|
1507
|
+
print("{:<20s} {:<10s}".format("Param","Tau (steps)"))
|
|
1508
|
+
print('\n'.join(["{:<20s} {:<10f}".format(*x) for x in zip(self.labels,e.tau)]))
|
|
1509
|
+
print("Nsteps this run = ",self.samples)
|
|
1510
|
+
print("Ideal nsteps >= ",int(50*np.max(e.tau)))
|
|
1511
|
+
|
|
1512
|
+
def save(self,fname=None):
|
|
1513
|
+
# inspired by the emcee hdf backend
|
|
1514
|
+
# could add a thin function here but best to apply in run() above
|
|
1515
|
+
if fname is None:
|
|
1516
|
+
if self.model.comment is not None:
|
|
1517
|
+
fname = self.model.comment +datetime.datetime.now().strftime("%Y-%m-%d-%H%M%S")+".hdf"
|
|
1518
|
+
print("Saving model to {}".format(fname))
|
|
1519
|
+
else:
|
|
1520
|
+
print("Please provide a filename: mcmc.save(fname='filename.hd5')")
|
|
1521
|
+
|
|
1522
|
+
with h5py.File(fname,'w') as f:
|
|
1523
|
+
g = f.create_group('mcmc')
|
|
1524
|
+
g.create_dataset('chain',data=self.sampler.backend.chain)
|
|
1525
|
+
g.create_dataset('accepted',data=self.sampler.backend.accepted)
|
|
1526
|
+
g.create_dataset('log_prob',data=self.sampler.backend.log_prob)
|
|
1527
|
+
|
|
1528
|
+
if self.sampler.backend.blobs is not None:
|
|
1529
|
+
g.create_dataset('blobs',data=self.sampler.backend.blobs)
|
|
1530
|
+
g.attrs['has_blobs'] = True
|
|
1531
|
+
else:
|
|
1532
|
+
g.attrs['has_blobs'] = False
|
|
1533
|
+
g.attrs['iteration'] = self.sampler.backend.iteration
|
|
1534
|
+
|
|
1535
|
+
|
|
1536
|
+
def load(self,fname):
|
|
1537
|
+
if self.sampler is None:
|
|
1538
|
+
print("Cannot load file, sampler does not exist")
|
|
1539
|
+
print("Run a short chain first, then load the data")
|
|
1540
|
+
# TODO auto-generate sampler based on file?
|
|
1541
|
+
|
|
1542
|
+
with h5py.File(fname,"r") as f:
|
|
1543
|
+
g = f['mcmc']
|
|
1544
|
+
self.sampler.backend.chain = g['chain'][:]
|
|
1545
|
+
self.sampler.backend.accepted = g['accepted'][:]
|
|
1546
|
+
if g.attrs['has_blobs'] is True:
|
|
1547
|
+
self.sampler.backend.blobs = g['blobs'][:]
|
|
1548
|
+
self.sampler.backend.iteration = g.attrs['iteration']
|
|
1549
|
+
self.sampler.backend.log_prob = g['log_prob'][:]
|
|
1550
|
+
# sampler.backend.random_state = g['random_state'][:]
|
|
1551
|
+
self.sampler.backend.initialized=True
|
|
1552
|
+
self.sampler._previous_state = self.sampler.get_last_sample()
|
|
1553
|
+
|
|
1554
|
+
|
|
1555
|
+
|
|
1556
|
+
|
|
1557
|
+
|
|
1558
|
+
|
|
1559
|
+
def doMCMC(model,obs,samples=1000,variance=0.1,walkers=10):
|
|
1560
|
+
print("Warning: doMCMC() is deprecated. Use the MCMC class and MCMC.run() in future.")
|
|
1561
|
+
mcmcModel = MCMC(model,obs,walkers=walkers,samples=samples,variance=variance)
|
|
1562
|
+
sampler,bm = mcmcModel.run(ret=True)
|
|
1563
|
+
return sampler,bm
|
|
1564
|
+
|
|
1565
|
+
|
|
1566
|
+
def plotMCMC(sampler,labels):
|
|
1567
|
+
print("Warning: plotMCMC() is deprecated. Use the MCMC class and MCMC.plot_chains() in future.")
|
|
1568
|
+
|
|
1569
|
+
ndim = sampler.ndim
|
|
1570
|
+
fig, axes = plt.subplots(ndim, figsize=(10, 7), sharex=True)
|
|
1571
|
+
samples = sampler.get_chain()
|
|
1572
|
+
#labels = ["k1","k2","lnsigma"] # TODO programmatically generate
|
|
1573
|
+
for i in range(ndim):
|
|
1574
|
+
ax = axes[i]
|
|
1575
|
+
ax.plot(samples[:, :, i], "k", alpha=0.3)
|
|
1576
|
+
ax.set_xlim(0, len(samples))
|
|
1577
|
+
ax.set_ylabel(labels[i])
|
|
1578
|
+
ax.yaxis.set_label_coords(-0.1, 0.5)
|
|
1579
|
+
|
|
1580
|
+
axes[-1].set_xlabel("step number")
|
|
1581
|
+
plt.show()
|
|
1582
|
+
|
|
1583
|
+
def plotCorner(sampler,labels):
|
|
1584
|
+
print("Warning: plotCorner() is deprecated. Use the MCMC class and MCMC.plot_corner() in future.")
|
|
1585
|
+
|
|
1586
|
+
try:
|
|
1587
|
+
tau = sampler.get_autocorr_time()
|
|
1588
|
+
except emcee.autocorr.AutocorrError as e:
|
|
1589
|
+
print("Warning: Autocorrelation time is likely too short. Check the chain.")
|
|
1590
|
+
tau=e.tau
|
|
1591
|
+
|
|
1592
|
+
burnin = int(2*np.max(tau))
|
|
1593
|
+
samples = sampler.get_chain(discard=burnin, flat=True)
|
|
1594
|
+
|
|
1595
|
+
corner.corner(samples,labels=labels,show_titles=True)
|
|
1596
|
+
plt.show()
|
|
1597
|
+
|
|
1598
|
+
def getTau(sampler):
|
|
1599
|
+
print("Warning: getTau is deprecated. Use the MCMC class and MCMC.get_tau() in future.")
|
|
1600
|
+
|
|
1601
|
+
try:
|
|
1602
|
+
tau = sampler.get_autocorr_time()
|
|
1603
|
+
print(tau)
|
|
1604
|
+
except emcee.autocorr.AutocorrError as e:
|
|
1605
|
+
print("Warning: Autocorrelation time is likely too short. Check the chain.")
|
|
1606
|
+
print(e.tau)
|
|
1607
|
+
|
|
1608
|
+
# TODO make this function into a method of bindingModel
|
|
1609
|
+
def makeMCMCLabels(model,obs):
|
|
1610
|
+
print("Warning: makeMCMCLabels is deprecated. Use the MCMC class and MCMC.labels in future.")
|
|
1611
|
+
|
|
1612
|
+
params = []
|
|
1613
|
+
for pp in model.params:
|
|
1614
|
+
if (model.params[pp].vary):
|
|
1615
|
+
params.append(model.params[pp].name)
|
|
1616
|
+
|
|
1617
|
+
|
|
1618
|
+
|
|
1619
|
+
ss = []
|
|
1620
|
+
for pp in obs:
|
|
1621
|
+
ss.append(pp.name) # todo - make sure everything (not just name) is the same, if not throw an error
|
|
1622
|
+
|
|
1623
|
+
ss = list(set(ss))
|
|
1624
|
+
ss = ['lnsigma'+x for x in ss]
|
|
1625
|
+
params += ss
|
|
1626
|
+
|
|
1627
|
+
return(params)
|
|
1628
|
+
|
|
1629
|
+
def mcmchelper(model,obsList=None,obs_types=None,walkers=25,samples=10000,variance=0.1,thin=500,figName='out.jpg'):
|
|
1630
|
+
"""
|
|
1631
|
+
Helper function to run MCMC on a binding model and plot results.
|
|
1632
|
+
|
|
1633
|
+
Parameters:
|
|
1634
|
+
- model: The binding model to run MCMC on.
|
|
1635
|
+
- obsList: List of observable types.
|
|
1636
|
+
- walkers: Number of walkers for MCMC.
|
|
1637
|
+
- samples: Number of samples to draw.
|
|
1638
|
+
- variance: Variance for the MCMC sampling.
|
|
1639
|
+
- thin: Thinning factor for the samples.
|
|
1640
|
+
- figName: Name of the output figure file.
|
|
1641
|
+
"""
|
|
1642
|
+
if obs_types is None:
|
|
1643
|
+
if obsList is None:
|
|
1644
|
+
raise ValueError("Either obsList or obs_types must be provided.")
|
|
1645
|
+
# If obs_types is not provided, create it based on obsList
|
|
1646
|
+
obs_types = [ObsType('deltaH')] * len(obsList)
|
|
1647
|
+
|
|
1648
|
+
# Create and run the MCMC sampler
|
|
1649
|
+
mcmc = MCMC(model, obs_types, walkers=walkers, samples=samples, variance=variance)
|
|
1650
|
+
mcmc.run(thin=thin)
|
|
1651
|
+
|
|
1652
|
+
# Plot results
|
|
1653
|
+
mcmc.plot_chain()
|
|
1654
|
+
mcmc.get_tau()
|
|
1655
|
+
ff = mcmc.plot_corner()
|
|
1656
|
+
ff.savefig(figName, dpi=150)
|
|
1657
|
+
return mcmc
|
|
1658
|
+
#sampler,bm=doMCMC(c8a,obsc8a)
|