bindtools 0.1.3__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
|
|
@@ -19,7 +19,7 @@ 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
|
|
@@ -35,7 +35,7 @@ dependencies = [
|
|
|
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",
|
|
@@ -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
|
|
265
|
+
|
|
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)
|
|
314
293
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
r1 = abs(h_tot - (h_free + hg + 2.0 * h2g))
|
|
323
|
-
r2 = abs(g_tot - (g_free + hg + h2g))
|
|
324
|
-
return r1 + r2
|
|
325
|
-
|
|
326
|
-
h_free = _best_real_root(coeffs, 0.0, max(h_tot, 0.0), score_fn)
|
|
327
|
-
d = max(1.0 + beta11 * h_free + beta21 * (h_free**2), 1e-300)
|
|
328
|
-
g_free = g_tot / d
|
|
329
|
-
hg = beta11 * h_free * g_free
|
|
330
|
-
h2g = beta21 * (h_free**2) * g_free
|
|
331
|
-
return max(h_free, 0.0), max(g_free, 0.0), max(hg, 0.0), max(h2g, 0.0)
|
|
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
|
|
|
@@ -796,6 +847,36 @@ def concToObservable(specConcs, specToInteg, specToDd, shiftParams, paramNames,
|
|
|
796
847
|
# return sum((specConcs.flatten() - res)**2)
|
|
797
848
|
|
|
798
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
|
+
|
|
799
880
|
class bindingModel:
|
|
800
881
|
def __init__(
|
|
801
882
|
self,
|
|
@@ -915,7 +996,108 @@ class bindingModel:
|
|
|
915
996
|
def setColumnToSpeciesMapping(self, colToSpec):
|
|
916
997
|
self.colToSpec = colToSpec
|
|
917
998
|
|
|
918
|
-
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
|
+
|
|
919
1101
|
for paramName in self.compNames:
|
|
920
1102
|
self._addParam("log" + paramName, init=0, vary=False)
|
|
921
1103
|
|
|
@@ -1401,6 +1583,7 @@ class MCMC:
|
|
|
1401
1583
|
self.variance = variance
|
|
1402
1584
|
self.sampler = None
|
|
1403
1585
|
self._labels = None
|
|
1586
|
+
self.thin = 1
|
|
1404
1587
|
|
|
1405
1588
|
@property
|
|
1406
1589
|
def labels(self):
|
|
@@ -1419,6 +1602,7 @@ class MCMC:
|
|
|
1419
1602
|
return params
|
|
1420
1603
|
|
|
1421
1604
|
def run(self, ret=False, thin=1, samples=None, pool=None, tqdm_kwargs=None):
|
|
1605
|
+
self.thin = thin
|
|
1422
1606
|
bm = self.model
|
|
1423
1607
|
bm.colTypes = self.obs
|
|
1424
1608
|
|
|
@@ -1484,7 +1668,7 @@ class MCMC:
|
|
|
1484
1668
|
)
|
|
1485
1669
|
else:
|
|
1486
1670
|
self.sampler.pool = active_pool
|
|
1487
|
-
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)
|
|
1488
1672
|
else:
|
|
1489
1673
|
if self.sampler is None:
|
|
1490
1674
|
self.sampler = emcee.EnsembleSampler(self.walkers, ndim, log_prob, args=[bm.fcn_opts, bounds])
|
|
@@ -1573,6 +1757,7 @@ class MCMC:
|
|
|
1573
1757
|
else:
|
|
1574
1758
|
g.attrs["has_blobs"] = False
|
|
1575
1759
|
g.attrs["iteration"] = self.sampler.backend.iteration
|
|
1760
|
+
g.attrs["thin"] = getattr(self, "thin", 1)
|
|
1576
1761
|
|
|
1577
1762
|
def load(self, fname):
|
|
1578
1763
|
if self.sampler is None:
|
|
@@ -1588,6 +1773,8 @@ class MCMC:
|
|
|
1588
1773
|
self.sampler.backend.blobs = g["blobs"][:]
|
|
1589
1774
|
self.sampler.backend.iteration = g.attrs["iteration"]
|
|
1590
1775
|
self.sampler.backend.log_prob = g["log_prob"][:]
|
|
1776
|
+
if "thin" in g.attrs:
|
|
1777
|
+
self.thin = int(g.attrs["thin"])
|
|
1591
1778
|
# sampler.backend.random_state = g['random_state'][:]
|
|
1592
1779
|
self.sampler.backend.initialized = True
|
|
1593
1780
|
self.sampler._previous_state = self.sampler.get_last_sample()
|
|
File without changes
|
|
File without changes
|