bindtools 0.1.2__tar.gz → 0.2.1__tar.gz
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.
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: bindtools
|
|
3
|
-
Version: 0.1
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Keywords: chemistry,analytical chemistry,binding constants,supramolecular
|
|
5
5
|
Author: Martin Peeks
|
|
6
6
|
Author-email: Martin Peeks <martinp23@googlemail.com>, m.peeks@unsw.edu.au
|
|
@@ -14,12 +14,12 @@ Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
|
|
|
14
14
|
Requires-Dist: arviz==0.21.0
|
|
15
15
|
Requires-Dist: corner==2.2.3
|
|
16
16
|
Requires-Dist: emcee==3.1.6
|
|
17
|
-
Requires-Dist: h5py
|
|
17
|
+
Requires-Dist: h5py>=3.14.0
|
|
18
18
|
Requires-Dist: lmfit==1.3.3
|
|
19
19
|
Requires-Dist: matplotlib==3.10.7
|
|
20
20
|
Requires-Dist: numba>=0.65.1
|
|
21
21
|
Requires-Dist: numpy>=2.3.4
|
|
22
|
-
Requires-Dist: openpyxl
|
|
22
|
+
Requires-Dist: openpyxl>=3.1.5
|
|
23
23
|
Requires-Dist: pandas>=2.3.3
|
|
24
24
|
Requires-Dist: scipy==1.16.3
|
|
25
25
|
Requires-Dist: tqdm==4.66.3
|
|
@@ -4,7 +4,7 @@ build-backend = "uv_build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "bindtools"
|
|
7
|
-
version = "0.1
|
|
7
|
+
version = "0.2.1"
|
|
8
8
|
readme = "README.md"
|
|
9
9
|
classifiers = [
|
|
10
10
|
# How mature is this project? Common values are
|
|
@@ -30,12 +30,12 @@ dependencies = [
|
|
|
30
30
|
"arviz==0.21.0",
|
|
31
31
|
"corner==2.2.3",
|
|
32
32
|
"emcee==3.1.6",
|
|
33
|
-
"h5py
|
|
33
|
+
"h5py>=3.14.0",
|
|
34
34
|
"lmfit==1.3.3",
|
|
35
35
|
"matplotlib==3.10.7",
|
|
36
36
|
"numba>=0.65.1",
|
|
37
37
|
"numpy>=2.3.4",
|
|
38
|
-
"openpyxl
|
|
38
|
+
"openpyxl>=3.1.5",
|
|
39
39
|
"pandas>=2.3.3",
|
|
40
40
|
"scipy==1.16.3",
|
|
41
41
|
"tqdm==4.66.3",
|
|
@@ -97,9 +97,9 @@ def getConcsScipy(eqMat, initComponentConc, logK, alg="L-BFGS-B"):
|
|
|
97
97
|
eqMat = eqMat.astype("float64")
|
|
98
98
|
K = 10.0**logK
|
|
99
99
|
guessCompConc = np.zeros(len(initComponentConc)) + np.mean(initComponentConc)
|
|
100
|
-
guessLogCompConc = np.
|
|
100
|
+
guessLogCompConc = np.log10(guessCompConc)
|
|
101
101
|
|
|
102
|
-
bds = [(None, np.
|
|
102
|
+
bds = [(None, np.log10(np.max(initComponentConc))) for _ in range(len(guessLogCompConc))]
|
|
103
103
|
# jac=True says that specObj returns float,arr(n) where arr(n) is the Jacobian
|
|
104
104
|
res = sp.optimize.minimize(
|
|
105
105
|
specObj,
|
|
@@ -112,7 +112,7 @@ def getConcsScipy(eqMat, initComponentConc, logK, alg="L-BFGS-B"):
|
|
|
112
112
|
options={"gtol": 1e-20},
|
|
113
113
|
)
|
|
114
114
|
|
|
115
|
-
compTotCalc, specConc = specCalc(
|
|
115
|
+
compTotCalc, specConc = specCalc(10**(res.x), len(K), eqMat, K)
|
|
116
116
|
return specConc
|
|
117
117
|
|
|
118
118
|
|
|
@@ -150,7 +150,7 @@ def _specJac(ncomp, eqMat, speciesConc): # this function calculates the Jacobia
|
|
|
150
150
|
|
|
151
151
|
|
|
152
152
|
def specObj(logConc, initComponentConc, eqMat, K):
|
|
153
|
-
conc =
|
|
153
|
+
conc = 10**(logConc)
|
|
154
154
|
nspecies = len(K)
|
|
155
155
|
compTotCalc, specConc = specCalc(conc, nspecies, eqMat, K)
|
|
156
156
|
|
|
@@ -228,107 +228,76 @@ def _analytical_obs_param_token(col_name: str) -> str:
|
|
|
228
228
|
return token
|
|
229
229
|
|
|
230
230
|
|
|
231
|
-
def _best_real_root(coeffs: np.ndarray, lower: float, upper: float, score_fn) -> float:
|
|
232
|
-
nz = np.where(np.abs(coeffs) > 1e-18)[0]
|
|
233
|
-
if len(nz) == 0:
|
|
234
|
-
return float(lower)
|
|
235
|
-
|
|
236
|
-
trimmed = coeffs[nz[0] :]
|
|
237
|
-
roots = np.roots(trimmed)
|
|
238
|
-
real_roots = [float(r.real) for r in roots if abs(r.imag) < 1e-10]
|
|
239
|
-
if not real_roots:
|
|
240
|
-
return float(lower)
|
|
241
|
-
|
|
242
|
-
bounded = [r for r in real_roots if lower - 1e-12 <= r <= upper + 1e-12]
|
|
243
|
-
candidates = bounded if bounded else real_roots
|
|
244
|
-
best = min(candidates, key=score_fn)
|
|
245
|
-
return float(np.clip(best, lower, upper))
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
def _solve_row_11(h_tot: float, g_tot: float, beta11: float) -> tuple[float, float, float]:
|
|
249
|
-
if beta11 <= 0:
|
|
250
|
-
raise ValueError("Binding constant (in natural units) cannot be negative")
|
|
251
|
-
if h_tot < 0 or g_tot < 0:
|
|
252
|
-
raise ValueError("Total concentrations must be non-negative")
|
|
253
|
-
if h_tot == 0 or g_tot == 0:
|
|
254
|
-
return (
|
|
255
|
-
max(h_tot, 0.0),
|
|
256
|
-
max(g_tot, 0.0),
|
|
257
|
-
0.0,
|
|
258
|
-
) # if either total concentration is zero, then there can be no complex
|
|
259
|
-
|
|
260
|
-
first_term = h_tot + g_tot + 1.0 / max(beta11, 1e-300)
|
|
261
|
-
hg = 0.5 * (first_term - math.sqrt(max(first_term**2 - 4.0 * h_tot * g_tot, 0.0)))
|
|
262
|
-
hg = float(
|
|
263
|
-
np.clip(hg, 0.0, min(h_tot, g_tot))
|
|
264
|
-
) # [HG] cannot be negative or greater than either total concentration
|
|
265
|
-
return h_tot - hg, g_tot - hg, hg
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
def _solve_row_12(h_tot: float, g_tot: float, beta11: float, beta12: float) -> tuple[float, float, float, float]:
|
|
269
|
-
if h_tot <= 0 or g_tot <= 0:
|
|
270
|
-
return max(h_tot, 0.0), max(g_tot, 0.0), 0.0, 0.0
|
|
271
|
-
|
|
272
|
-
coeffs = np.array(
|
|
273
|
-
[
|
|
274
|
-
beta12,
|
|
275
|
-
beta11 + 2.0 * h_tot * beta12 - g_tot * beta12,
|
|
276
|
-
1.0 + h_tot * beta11 - g_tot * beta11,
|
|
277
|
-
-g_tot,
|
|
278
|
-
],
|
|
279
|
-
dtype=float,
|
|
280
|
-
)
|
|
281
231
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
232
|
+
def _solve_cubic_vectorized(a: np.ndarray, b: np.ndarray, c: np.ndarray, d: np.ndarray, lower: np.ndarray, upper: np.ndarray, score_fn_list) -> np.ndarray:
|
|
233
|
+
A = b / a
|
|
234
|
+
B = c / a
|
|
235
|
+
C = d / a
|
|
236
|
+
|
|
237
|
+
p = B - A**2 / 3.0
|
|
238
|
+
q = C - A * B / 3.0 + 2.0 * A**3 / 27.0
|
|
239
|
+
|
|
240
|
+
D = q**2 / 4.0 + p**3 / 27.0
|
|
241
|
+
|
|
242
|
+
N = len(a)
|
|
243
|
+
candidates = np.zeros((3, N), dtype=float)
|
|
244
|
+
|
|
245
|
+
mask_single = D > 0
|
|
246
|
+
mask_three = ~mask_single
|
|
247
|
+
|
|
248
|
+
# Solve single root cases (D > 0)
|
|
249
|
+
if np.any(mask_single):
|
|
250
|
+
D_s = D[mask_single]
|
|
251
|
+
q_s = q[mask_single]
|
|
252
|
+
A_s = A[mask_single]
|
|
253
|
+
|
|
254
|
+
sqrt_D = np.sqrt(D_s)
|
|
255
|
+
val1 = -q_s / 2.0 + sqrt_D
|
|
256
|
+
val2 = -q_s / 2.0 - sqrt_D
|
|
257
|
+
|
|
258
|
+
u = np.sign(val1) * np.abs(val1)**(1.0/3.0)
|
|
259
|
+
v = np.sign(val2) * np.abs(val2)**(1.0/3.0)
|
|
260
|
+
|
|
261
|
+
root_s = u + v - A_s / 3.0
|
|
262
|
+
candidates[0, mask_single] = root_s
|
|
263
|
+
candidates[1, mask_single] = root_s
|
|
264
|
+
candidates[2, mask_single] = root_s
|
|
314
265
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
266
|
+
# Solve three root cases (D <= 0)
|
|
267
|
+
if np.any(mask_three):
|
|
268
|
+
p_t = p[mask_three]
|
|
269
|
+
q_t = q[mask_three]
|
|
270
|
+
A_t = A[mask_three]
|
|
271
|
+
|
|
272
|
+
valid = -p_t > 1e-12
|
|
273
|
+
phi = np.zeros_like(p_t)
|
|
274
|
+
r = np.zeros_like(p_t)
|
|
275
|
+
if np.any(valid):
|
|
276
|
+
p_v = p_t[valid]
|
|
277
|
+
q_v = q_t[valid]
|
|
278
|
+
cos_phi = (3.0 * q_v) / (2.0 * p_v * np.sqrt(-p_v / 3.0))
|
|
279
|
+
cos_phi = np.clip(cos_phi, -1.0, 1.0)
|
|
280
|
+
phi[valid] = np.arccos(cos_phi)
|
|
281
|
+
r[valid] = 2.0 * np.sqrt(-p_v / 3.0)
|
|
282
|
+
|
|
283
|
+
y0 = r * np.cos(phi / 3.0)
|
|
284
|
+
y1 = r * np.cos(phi / 3.0 + 2.0 * np.pi / 3.0)
|
|
285
|
+
y2 = r * np.cos(phi / 3.0 + 4.0 * np.pi / 3.0)
|
|
286
|
+
|
|
287
|
+
candidates[0, mask_three] = y0 - A_t / 3.0
|
|
288
|
+
candidates[1, mask_three] = y1 - A_t / 3.0
|
|
289
|
+
candidates[2, mask_three] = y2 - A_t / 3.0
|
|
290
|
+
|
|
291
|
+
# Evaluate residuals using the vectorized score function
|
|
292
|
+
residuals = score_fn_list(candidates)
|
|
293
|
+
|
|
294
|
+
# Penalize out of bounds candidates
|
|
295
|
+
out_of_bounds = (candidates < lower - 1e-12) | (candidates > upper + 1e-12)
|
|
296
|
+
residuals[out_of_bounds] += 1e10
|
|
297
|
+
|
|
298
|
+
best_idx = np.argmin(residuals, axis=0)
|
|
299
|
+
best_root = candidates[best_idx, np.arange(N)]
|
|
300
|
+
return np.clip(best_root, lower, upper)
|
|
332
301
|
|
|
333
302
|
|
|
334
303
|
def calc_analytical_speciation(
|
|
@@ -340,46 +309,128 @@ def calc_analytical_speciation(
|
|
|
340
309
|
complex_indices: list[int],
|
|
341
310
|
) -> tuple[np.ndarray, bool]:
|
|
342
311
|
|
|
312
|
+
if n_comp != 2:
|
|
313
|
+
raise ValueError("Analytical fast-exchange solver requires exactly 2 components.")
|
|
314
|
+
|
|
343
315
|
n_rows = int(np.shape(comp_concs)[0])
|
|
344
316
|
n_species = int(np.shape(eq_mat)[1])
|
|
345
317
|
spec_calc = np.zeros((n_rows, n_species), dtype=float)
|
|
346
318
|
error = False
|
|
347
319
|
|
|
348
|
-
|
|
349
|
-
|
|
320
|
+
h_tot = np.maximum(comp_concs[:, 0], 0.0)
|
|
321
|
+
g_tot = np.maximum(comp_concs[:, 1], 0.0)
|
|
322
|
+
zero_mask = (h_tot <= 0.0) | (g_tot <= 0.0)
|
|
350
323
|
|
|
351
324
|
beta11 = 10.0 ** float(binding_params[complex_indices[0]])
|
|
352
|
-
beta2 = None
|
|
353
|
-
if topology in ("1:2", "2:1"):
|
|
354
|
-
beta2 = 10.0 ** float(binding_params[complex_indices[1]])
|
|
355
325
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
h_free, g_free, hg = _solve_row_11(h_tot, g_tot, beta11)
|
|
362
|
-
spec_calc[ii, 0] = h_free
|
|
363
|
-
spec_calc[ii, 1] = g_free
|
|
364
|
-
spec_calc[ii, complex_indices[0]] = hg
|
|
365
|
-
elif topology == "1:2":
|
|
366
|
-
h_free, g_free, hg, hg2 = _solve_row_12(h_tot, g_tot, beta11, float(beta2))
|
|
367
|
-
spec_calc[ii, 0] = h_free
|
|
368
|
-
spec_calc[ii, 1] = g_free
|
|
369
|
-
spec_calc[ii, complex_indices[0]] = hg
|
|
370
|
-
spec_calc[ii, complex_indices[1]] = hg2
|
|
371
|
-
elif topology == "2:1":
|
|
372
|
-
h_free, g_free, hg, h2g = _solve_row_21(h_tot, g_tot, beta11, float(beta2))
|
|
373
|
-
spec_calc[ii, 0] = h_free
|
|
374
|
-
spec_calc[ii, 1] = g_free
|
|
375
|
-
spec_calc[ii, complex_indices[0]] = hg
|
|
376
|
-
spec_calc[ii, complex_indices[1]] = h2g
|
|
326
|
+
try:
|
|
327
|
+
if topology == "1:1":
|
|
328
|
+
if beta11 <= 0:
|
|
329
|
+
spec_calc[:, 0] = h_tot
|
|
330
|
+
spec_calc[:, 1] = g_tot
|
|
377
331
|
else:
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
332
|
+
first_term = h_tot + g_tot + 1.0 / max(beta11, 1e-300)
|
|
333
|
+
sqrt_val = np.sqrt(np.maximum(first_term**2 - 4.0 * h_tot * g_tot, 0.0))
|
|
334
|
+
hg = 0.5 * (first_term - sqrt_val)
|
|
335
|
+
hg = np.clip(hg, 0.0, np.minimum(h_tot, g_tot))
|
|
336
|
+
hg[zero_mask] = 0.0
|
|
337
|
+
|
|
338
|
+
spec_calc[:, 0] = np.maximum(h_tot - hg, 0.0)
|
|
339
|
+
spec_calc[:, 1] = np.maximum(g_tot - hg, 0.0)
|
|
340
|
+
spec_calc[:, complex_indices[0]] = hg
|
|
341
|
+
|
|
342
|
+
elif topology == "1:2":
|
|
343
|
+
beta12 = 10.0 ** float(binding_params[complex_indices[1]])
|
|
344
|
+
|
|
345
|
+
# Setup coefficients for cubic equation a*g^3 + b*g^2 + c*g + d = 0
|
|
346
|
+
a = np.full(n_rows, beta12, dtype=float)
|
|
347
|
+
b = beta11 + 2.0 * h_tot * beta12 - g_tot * beta12
|
|
348
|
+
c = 1.0 + h_tot * beta11 - g_tot * beta11
|
|
349
|
+
d = -g_tot
|
|
350
|
+
|
|
351
|
+
def score_12(g_free_candidates):
|
|
352
|
+
h_t = h_tot[np.newaxis, :]
|
|
353
|
+
g_t = g_tot[np.newaxis, :]
|
|
354
|
+
denom = np.maximum(1.0 + beta11 * g_free_candidates + beta12 * (g_free_candidates**2), 1e-300)
|
|
355
|
+
h_f = h_t / denom
|
|
356
|
+
hg = beta11 * h_f * g_free_candidates
|
|
357
|
+
hg2 = beta12 * h_f * (g_free_candidates**2)
|
|
358
|
+
r1 = np.abs(h_t - (h_f + hg + hg2))
|
|
359
|
+
r2 = np.abs(g_t - (g_free_candidates + hg + 2.0 * hg2))
|
|
360
|
+
return r1 + r2
|
|
361
|
+
|
|
362
|
+
g_free = _solve_cubic_vectorized(a, b, c, d, np.zeros(n_rows), g_tot, score_12)
|
|
363
|
+
denom = np.maximum(1.0 + beta11 * g_free + beta12 * (g_free**2), 1e-300)
|
|
364
|
+
h_free = h_tot / denom
|
|
365
|
+
hg = beta11 * h_free * g_free
|
|
366
|
+
hg2 = beta12 * h_free * (g_free**2)
|
|
367
|
+
|
|
368
|
+
# Apply zero/negative overrides
|
|
369
|
+
h_free = np.maximum(h_free, 0.0)
|
|
370
|
+
g_free = np.maximum(g_free, 0.0)
|
|
371
|
+
hg = np.maximum(hg, 0.0)
|
|
372
|
+
hg2 = np.maximum(hg2, 0.0)
|
|
373
|
+
|
|
374
|
+
h_free[zero_mask] = h_tot[zero_mask]
|
|
375
|
+
g_free[zero_mask] = g_tot[zero_mask]
|
|
376
|
+
hg[zero_mask] = 0.0
|
|
377
|
+
hg2[zero_mask] = 0.0
|
|
378
|
+
|
|
379
|
+
spec_calc[:, 0] = h_free
|
|
380
|
+
spec_calc[:, 1] = g_free
|
|
381
|
+
spec_calc[:, complex_indices[0]] = hg
|
|
382
|
+
spec_calc[:, complex_indices[1]] = hg2
|
|
383
|
+
|
|
384
|
+
elif topology == "2:1":
|
|
385
|
+
beta21 = 10.0 ** float(binding_params[complex_indices[1]])
|
|
386
|
+
|
|
387
|
+
# Setup coefficients for cubic equation a*h^3 + b*h^2 + c*h + d = 0
|
|
388
|
+
a = np.full(n_rows, beta21, dtype=float)
|
|
389
|
+
b = beta11 + 2.0 * g_tot * beta21 - h_tot * beta21
|
|
390
|
+
c = 1.0 + g_tot * beta11 - h_tot * beta11
|
|
391
|
+
d = -h_tot
|
|
392
|
+
|
|
393
|
+
def score_21(h_free_candidates):
|
|
394
|
+
h_t = h_tot[np.newaxis, :]
|
|
395
|
+
g_t = g_tot[np.newaxis, :]
|
|
396
|
+
denom = np.maximum(1.0 + beta11 * h_free_candidates + beta21 * (h_free_candidates**2), 1e-300)
|
|
397
|
+
g_f = g_t / denom
|
|
398
|
+
hg = beta11 * h_free_candidates * g_f
|
|
399
|
+
h2g = beta21 * (h_free_candidates**2) * g_f
|
|
400
|
+
r1 = np.abs(h_t - (h_free_candidates + hg + 2.0 * h2g))
|
|
401
|
+
r2 = np.abs(g_t - (g_f + hg + h2g))
|
|
402
|
+
return r1 + r2
|
|
403
|
+
|
|
404
|
+
h_free = _solve_cubic_vectorized(a, b, c, d, np.zeros(n_rows), h_tot, score_21)
|
|
405
|
+
denom = np.maximum(1.0 + beta11 * h_free + beta21 * (h_free**2), 1e-300)
|
|
406
|
+
g_free = g_tot / denom
|
|
407
|
+
hg = beta11 * h_free * g_free
|
|
408
|
+
h2g = beta21 * (h_free**2) * g_free
|
|
409
|
+
|
|
410
|
+
# Apply zero/negative overrides
|
|
411
|
+
h_free = np.maximum(h_free, 0.0)
|
|
412
|
+
g_free = np.maximum(g_free, 0.0)
|
|
413
|
+
hg = np.maximum(hg, 0.0)
|
|
414
|
+
h2g = np.maximum(h2g, 0.0)
|
|
415
|
+
|
|
416
|
+
h_free[zero_mask] = h_tot[zero_mask]
|
|
417
|
+
g_free[zero_mask] = g_tot[zero_mask]
|
|
418
|
+
hg[zero_mask] = 0.0
|
|
419
|
+
h2g[zero_mask] = 0.0
|
|
420
|
+
|
|
421
|
+
spec_calc[:, 0] = h_free
|
|
422
|
+
spec_calc[:, 1] = g_free
|
|
423
|
+
spec_calc[:, complex_indices[0]] = hg
|
|
424
|
+
spec_calc[:, complex_indices[1]] = h2g
|
|
425
|
+
|
|
426
|
+
else:
|
|
427
|
+
raise ValueError(f"Unsupported analytical topology: {topology}")
|
|
428
|
+
except Exception:
|
|
429
|
+
error = True
|
|
430
|
+
# Fallback to numerical solver row-by-row
|
|
431
|
+
for ii in range(n_rows):
|
|
381
432
|
try:
|
|
382
|
-
spec_calc[ii, :] = getConcs(eq_mat,
|
|
433
|
+
spec_calc[ii, :] = getConcs(eq_mat, comp_concs[ii], binding_params)
|
|
383
434
|
except Exception:
|
|
384
435
|
spec_calc[ii, :] = 0.0
|
|
385
436
|
|
|
@@ -567,15 +618,29 @@ def fitfun(params, fcn_opts): # ,eqMat,specConcs,startShifts=None,sigma=1,ret='
|
|
|
567
618
|
|
|
568
619
|
bindingParams = np.array([*parvals][: fcn_opts["nK"]], dtype=np.float64)
|
|
569
620
|
error = False
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
621
|
+
|
|
622
|
+
analytical_topology = fcn_opts.get("analytical_topology")
|
|
623
|
+
if analytical_topology in ("1:1", "1:2", "2:1"):
|
|
624
|
+
comp_concs = np.array(fcn_opts["compConcs"], dtype=float)
|
|
625
|
+
complex_indices = [int(x) for x in fcn_opts.get("analytical_complex_indices", [])]
|
|
626
|
+
specCalc, error = calc_analytical_speciation(
|
|
627
|
+
comp_concs=comp_concs,
|
|
628
|
+
eq_mat=np.array(fcn_opts["eqMat"], dtype=float),
|
|
629
|
+
binding_params=bindingParams,
|
|
630
|
+
topology=str(analytical_topology),
|
|
631
|
+
n_comp=int(comp_concs.shape[1]),
|
|
632
|
+
complex_indices=complex_indices,
|
|
633
|
+
)
|
|
634
|
+
else:
|
|
635
|
+
specCalc = []
|
|
636
|
+
for row in fcn_opts["compConcs"]:
|
|
637
|
+
try:
|
|
638
|
+
yEq = getConcs(fcn_opts["eqMat"], row, bindingParams)
|
|
639
|
+
except EquilibriumError as e:
|
|
640
|
+
yEq = e.val
|
|
641
|
+
error = True
|
|
642
|
+
specCalc.append(yEq) # yEq[4:])
|
|
643
|
+
specCalc = np.array(specCalc)
|
|
579
644
|
# except EquilibriumError as e:
|
|
580
645
|
# if fcn_opts['ret'] == 'residual':
|
|
581
646
|
# return 1000
|
|
@@ -782,6 +847,36 @@ def concToObservable(specConcs, specToInteg, specToDd, shiftParams, paramNames,
|
|
|
782
847
|
# return sum((specConcs.flatten() - res)**2)
|
|
783
848
|
|
|
784
849
|
|
|
850
|
+
def _infer_simple_fast_exchange_topology(eq_mat: np.ndarray, n_comp: int) -> Optional[tuple[str, list[int]]]:
|
|
851
|
+
if not isinstance(eq_mat, np.ndarray) or eq_mat.ndim != 2:
|
|
852
|
+
return None
|
|
853
|
+
if n_comp != 2 or eq_mat.shape[0] != 2 or eq_mat.shape[1] <= n_comp:
|
|
854
|
+
return None
|
|
855
|
+
|
|
856
|
+
bound_indices = list(range(n_comp, eq_mat.shape[1]))
|
|
857
|
+
sig_to_idx = {}
|
|
858
|
+
for idx in bound_indices:
|
|
859
|
+
a = eq_mat[0, idx]
|
|
860
|
+
b = eq_mat[1, idx]
|
|
861
|
+
if not np.isfinite(a) or not np.isfinite(b):
|
|
862
|
+
return None
|
|
863
|
+
if not np.isclose(a, round(a)) or not np.isclose(b, round(b)):
|
|
864
|
+
return None
|
|
865
|
+
sig = (int(round(a)), int(round(b)))
|
|
866
|
+
if sig in sig_to_idx:
|
|
867
|
+
return None
|
|
868
|
+
sig_to_idx[sig] = idx
|
|
869
|
+
|
|
870
|
+
sigs = set(sig_to_idx.keys())
|
|
871
|
+
if len(bound_indices) == 1 and sigs == {(1, 1)}:
|
|
872
|
+
return "1:1", [sig_to_idx[(1, 1)]]
|
|
873
|
+
if len(bound_indices) == 2 and sigs == {(1, 1), (1, 2)}:
|
|
874
|
+
return "1:2", [sig_to_idx[(1, 1)], sig_to_idx[(1, 2)]]
|
|
875
|
+
if len(bound_indices) == 2 and sigs == {(1, 1), (2, 1)}:
|
|
876
|
+
return "2:1", [sig_to_idx[(1, 1)], sig_to_idx[(2, 1)]]
|
|
877
|
+
return None
|
|
878
|
+
|
|
879
|
+
|
|
785
880
|
class bindingModel:
|
|
786
881
|
def __init__(
|
|
787
882
|
self,
|
|
@@ -901,7 +996,108 @@ class bindingModel:
|
|
|
901
996
|
def setColumnToSpeciesMapping(self, colToSpec):
|
|
902
997
|
self.colToSpec = colToSpec
|
|
903
998
|
|
|
904
|
-
def prepModel(self):
|
|
999
|
+
def prepModel(self, force_numerical=False):
|
|
1000
|
+
if force_numerical:
|
|
1001
|
+
self.analytical_fast_exchange = False
|
|
1002
|
+
self.analytical_topology = None
|
|
1003
|
+
self.analytical_obs_columns = []
|
|
1004
|
+
self.analytical_obs_components = []
|
|
1005
|
+
self.analytical_complex_indices = []
|
|
1006
|
+
self.analytical_obs_param_map = []
|
|
1007
|
+
self.analytical_linear_obs_columns = []
|
|
1008
|
+
self.analytical_linear_obs_param_map = []
|
|
1009
|
+
else:
|
|
1010
|
+
if not self.analytical_topology:
|
|
1011
|
+
topology_res = _infer_simple_fast_exchange_topology(self.eqMat, len(self.compNames))
|
|
1012
|
+
if topology_res is not None:
|
|
1013
|
+
self.analytical_topology, self.analytical_complex_indices = topology_res
|
|
1014
|
+
|
|
1015
|
+
if self.analytical_topology in ("1:1", "1:2", "2:1") and not self.analytical_fast_exchange:
|
|
1016
|
+
# 1. Check slow exchange mapping (specToInteg / colToSpec)
|
|
1017
|
+
has_slow_exchange = False
|
|
1018
|
+
if self.colToSpec is not None and self.colToSpec.size > 0:
|
|
1019
|
+
try:
|
|
1020
|
+
flat_vals = []
|
|
1021
|
+
for x in self.colToSpec.flat:
|
|
1022
|
+
try:
|
|
1023
|
+
val = float(x)
|
|
1024
|
+
except (TypeError, ValueError):
|
|
1025
|
+
val = 0.0
|
|
1026
|
+
flat_vals.append(val)
|
|
1027
|
+
flat_arr = np.array(flat_vals)
|
|
1028
|
+
if np.any(~np.isclose(flat_arr, 0.0)):
|
|
1029
|
+
has_slow_exchange = True
|
|
1030
|
+
except Exception:
|
|
1031
|
+
pass
|
|
1032
|
+
|
|
1033
|
+
# 2. Check types of columns (NMR vs Linear vs Mixed)
|
|
1034
|
+
n_integ = self.colToSpec.shape[1] if (self.colToSpec is not None and self.colToSpec.size > 0) else 0
|
|
1035
|
+
n_linear = self.specToLinear.shape[1] if (self.specToLinear is not None and self.specToLinear.size > 0) else 0
|
|
1036
|
+
n_shift = self.specToDd.shape[1] if (self.specToDd is not None and self.specToDd.size > 0) else 0
|
|
1037
|
+
|
|
1038
|
+
obs_list = self.obsList if self.obsList is not None else []
|
|
1039
|
+
if n_shift == 0:
|
|
1040
|
+
n_shift = max(0, len(obs_list) - n_integ - n_linear)
|
|
1041
|
+
|
|
1042
|
+
if len(obs_list) < n_integ + n_linear + n_shift:
|
|
1043
|
+
obs_list = list(obs_list) + [f"obs_{i}" for i in range(len(obs_list), n_integ + n_linear + n_shift)]
|
|
1044
|
+
|
|
1045
|
+
linear_cols = obs_list[n_integ:n_integ + n_linear]
|
|
1046
|
+
shift_cols = obs_list[n_integ + n_linear:n_integ + n_linear + n_shift]
|
|
1047
|
+
|
|
1048
|
+
|
|
1049
|
+
if not has_slow_exchange and not (n_shift > 0 and n_linear > 0) and (n_shift > 0 or n_linear > 0):
|
|
1050
|
+
self.analytical_fast_exchange = True
|
|
1051
|
+
|
|
1052
|
+
if n_linear > 0:
|
|
1053
|
+
self.analytical_linear_obs_columns = linear_cols
|
|
1054
|
+
linear_obs_param_map = []
|
|
1055
|
+
for obs_idx in range(n_linear):
|
|
1056
|
+
pnames = []
|
|
1057
|
+
for species_idx in range(self.specToLinear.shape[0]):
|
|
1058
|
+
cell = self.specToLinear[species_idx, obs_idx]
|
|
1059
|
+
if isinstance(cell, lmfit.Parameter):
|
|
1060
|
+
pnames.append(cell.name)
|
|
1061
|
+
else:
|
|
1062
|
+
pnames.append(None)
|
|
1063
|
+
linear_obs_param_map.append(pnames)
|
|
1064
|
+
self.analytical_linear_obs_param_map = linear_obs_param_map
|
|
1065
|
+
self.analytical_obs_columns = []
|
|
1066
|
+
self.analytical_obs_components = []
|
|
1067
|
+
else:
|
|
1068
|
+
self.analytical_obs_columns = shift_cols
|
|
1069
|
+
|
|
1070
|
+
def is_nonzero_mapping(cell):
|
|
1071
|
+
if cell is None:
|
|
1072
|
+
return False
|
|
1073
|
+
if isinstance(cell, (int, float)):
|
|
1074
|
+
return not np.isclose(cell, 0.0)
|
|
1075
|
+
return True
|
|
1076
|
+
|
|
1077
|
+
obs_components = []
|
|
1078
|
+
for obs_idx in range(n_shift):
|
|
1079
|
+
has_comp0 = False
|
|
1080
|
+
has_comp1 = False
|
|
1081
|
+
if self.specToDd is not None and self.specToDd.shape[0] > 1:
|
|
1082
|
+
has_comp0 = is_nonzero_mapping(self.specToDd[0, obs_idx])
|
|
1083
|
+
has_comp1 = is_nonzero_mapping(self.specToDd[1, obs_idx])
|
|
1084
|
+
|
|
1085
|
+
if has_comp0 and not has_comp1:
|
|
1086
|
+
obs_components.append(0)
|
|
1087
|
+
elif has_comp1 and not has_comp0:
|
|
1088
|
+
obs_components.append(1)
|
|
1089
|
+
else:
|
|
1090
|
+
col_name = shift_cols[obs_idx] if obs_idx < len(shift_cols) else ""
|
|
1091
|
+
tokens = [t for t in re.split(r"[^A-Za-z0-9]+", col_name.lower()) if t]
|
|
1092
|
+
matches = [idx for idx, comp in enumerate(self.compNames) if str(comp).lower() in tokens]
|
|
1093
|
+
if len(matches) == 1:
|
|
1094
|
+
obs_components.append(matches[0])
|
|
1095
|
+
else:
|
|
1096
|
+
obs_components.append(obs_idx % 2)
|
|
1097
|
+
self.analytical_obs_components = obs_components
|
|
1098
|
+
self.analytical_linear_obs_columns = []
|
|
1099
|
+
self.analytical_linear_obs_param_map = []
|
|
1100
|
+
|
|
905
1101
|
for paramName in self.compNames:
|
|
906
1102
|
self._addParam("log" + paramName, init=0, vary=False)
|
|
907
1103
|
|
|
@@ -1387,6 +1583,7 @@ class MCMC:
|
|
|
1387
1583
|
self.variance = variance
|
|
1388
1584
|
self.sampler = None
|
|
1389
1585
|
self._labels = None
|
|
1586
|
+
self.thin = 1
|
|
1390
1587
|
|
|
1391
1588
|
@property
|
|
1392
1589
|
def labels(self):
|
|
@@ -1405,6 +1602,7 @@ class MCMC:
|
|
|
1405
1602
|
return params
|
|
1406
1603
|
|
|
1407
1604
|
def run(self, ret=False, thin=1, samples=None, pool=None, tqdm_kwargs=None):
|
|
1605
|
+
self.thin = thin
|
|
1408
1606
|
bm = self.model
|
|
1409
1607
|
bm.colTypes = self.obs
|
|
1410
1608
|
|
|
@@ -1470,7 +1668,7 @@ class MCMC:
|
|
|
1470
1668
|
)
|
|
1471
1669
|
else:
|
|
1472
1670
|
self.sampler.pool = active_pool
|
|
1473
|
-
self.sampler.run_mcmc(p0, samples, progress=True, progress_kwargs=tqdm_kwargs)
|
|
1671
|
+
self.sampler.run_mcmc(p0, samples, progress=True, thin_by=thin, progress_kwargs=tqdm_kwargs)
|
|
1474
1672
|
else:
|
|
1475
1673
|
if self.sampler is None:
|
|
1476
1674
|
self.sampler = emcee.EnsembleSampler(self.walkers, ndim, log_prob, args=[bm.fcn_opts, bounds])
|
|
@@ -1559,6 +1757,7 @@ class MCMC:
|
|
|
1559
1757
|
else:
|
|
1560
1758
|
g.attrs["has_blobs"] = False
|
|
1561
1759
|
g.attrs["iteration"] = self.sampler.backend.iteration
|
|
1760
|
+
g.attrs["thin"] = getattr(self, "thin", 1)
|
|
1562
1761
|
|
|
1563
1762
|
def load(self, fname):
|
|
1564
1763
|
if self.sampler is None:
|
|
@@ -1574,6 +1773,8 @@ class MCMC:
|
|
|
1574
1773
|
self.sampler.backend.blobs = g["blobs"][:]
|
|
1575
1774
|
self.sampler.backend.iteration = g.attrs["iteration"]
|
|
1576
1775
|
self.sampler.backend.log_prob = g["log_prob"][:]
|
|
1776
|
+
if "thin" in g.attrs:
|
|
1777
|
+
self.thin = int(g.attrs["thin"])
|
|
1577
1778
|
# sampler.backend.random_state = g['random_state'][:]
|
|
1578
1779
|
self.sampler.backend.initialized = True
|
|
1579
1780
|
self.sampler._previous_state = self.sampler.get_last_sample()
|
|
File without changes
|
|
File without changes
|