bindtools 0.2.1__tar.gz → 0.2.3__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.2.1
3
+ Version: 0.2.3
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
@@ -11,18 +11,18 @@ Classifier: Programming Language :: Python :: 3.13
11
11
  Classifier: Programming Language :: Python :: 3.14
12
12
  Classifier: Topic :: Scientific/Engineering :: Chemistry
13
13
  Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
14
- Requires-Dist: arviz==0.21.0
15
14
  Requires-Dist: corner==2.2.3
16
15
  Requires-Dist: emcee==3.1.6
17
16
  Requires-Dist: h5py>=3.14.0
18
- Requires-Dist: lmfit==1.3.3
19
- Requires-Dist: matplotlib==3.10.7
20
- Requires-Dist: numba>=0.65.1
17
+ Requires-Dist: latex2mathml>=3.81.0
18
+ Requires-Dist: lmfit==1.3.4
19
+ Requires-Dist: matplotlib==3.11.0
20
+ Requires-Dist: numba>=0.66
21
21
  Requires-Dist: numpy>=2.3.4
22
22
  Requires-Dist: openpyxl>=3.1.5
23
23
  Requires-Dist: pandas>=2.3.3
24
- Requires-Dist: scipy==1.16.3
25
- Requires-Dist: tqdm==4.66.3
24
+ Requires-Dist: scipy==1.18.0
25
+ Requires-Dist: tqdm==4.68.3
26
26
  Requires-Dist: uncertainties==3.2.3
27
27
  Requires-Python: >=3.12
28
28
  Project-URL: Homepage, https://github.com/martinp23/bindtools
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "bindtools"
7
- version = "0.2.1"
7
+ version = "0.2.3"
8
8
  readme = "README.md"
9
9
  classifiers = [
10
10
  # How mature is this project? Common values are
@@ -27,18 +27,18 @@ classifiers = [
27
27
  requires-python = ">=3.12"
28
28
  keywords = ["chemistry", "analytical chemistry", "binding constants", "supramolecular"]
29
29
  dependencies = [
30
- "arviz==0.21.0",
31
30
  "corner==2.2.3",
32
31
  "emcee==3.1.6",
33
32
  "h5py>=3.14.0",
34
- "lmfit==1.3.3",
35
- "matplotlib==3.10.7",
36
- "numba>=0.65.1",
33
+ "latex2mathml>=3.81.0",
34
+ "lmfit==1.3.4",
35
+ "matplotlib==3.11.0",
36
+ "numba>=0.66",
37
37
  "numpy>=2.3.4",
38
38
  "openpyxl>=3.1.5",
39
39
  "pandas>=2.3.3",
40
- "scipy==1.16.3",
41
- "tqdm==4.66.3",
40
+ "scipy==1.18.0",
41
+ "tqdm==4.68.3",
42
42
  "uncertainties==3.2.3",
43
43
  ]
44
44
  authors = [
@@ -133,5 +133,7 @@ docstring-code-line-length = "dynamic"
133
133
 
134
134
  [dependency-groups]
135
135
  dev = [
136
+ "ipykernel>=7.3.0",
136
137
  "pytest>=9.1.0",
138
+ "ruff>=0.15.18",
137
139
  ]
@@ -5,7 +5,6 @@
5
5
  import datetime
6
6
  import math
7
7
  import os
8
- import re
9
8
  from multiprocessing import Pool
10
9
  from contextlib import nullcontext
11
10
 
@@ -218,16 +217,6 @@ def DoNR(eqMat, K, initComponentConc, guessCompConc):
218
217
  )
219
218
 
220
219
 
221
- def _analytical_obs_param_token(col_name: str) -> str:
222
- token = re.sub(r"[^\w]+", "_", str(col_name))
223
- token = re.sub(r"_+", "_", token).strip("_")
224
- if not token:
225
- token = "obs"
226
- if not (token[0].isalpha() or token[0] == "_"):
227
- token = f"obs_{token}"
228
- return token
229
-
230
-
231
220
 
232
221
  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
222
  A = b / a
@@ -437,162 +426,6 @@ def calc_analytical_speciation(
437
426
  return spec_calc, error
438
427
 
439
428
 
440
- def _analytical_param_value(values: np.ndarray, name_to_idx: dict[str, int], param_name: str) -> float:
441
- idx = name_to_idx.get(param_name)
442
- if idx is None or idx >= len(values):
443
- return 0.0
444
- return float(values[idx])
445
-
446
-
447
- def calc_analytical_observables(
448
- spec_calc: np.ndarray,
449
- comp_concs: np.ndarray,
450
- eq_mat: np.ndarray,
451
- obs_components: list[int],
452
- complex_indices: list[int],
453
- shift_param_values: np.ndarray,
454
- shift_param_names: list[str],
455
- obs_param_map: list[list[str]],
456
- ) -> np.ndarray:
457
-
458
- n_obs = len(obs_components)
459
- out = np.zeros((spec_calc.shape[0], n_obs), dtype=float)
460
- name_to_idx = {name: i for i, name in enumerate(shift_param_names)}
461
-
462
- for obs_idx in range(n_obs):
463
- comp_idx = int(obs_components[obs_idx])
464
- denom = comp_concs[:, comp_idx]
465
- pnames = obs_param_map[obs_idx]
466
- baseline = _analytical_param_value(shift_param_values, name_to_idx, pnames[0])
467
- out[:, obs_idx] = baseline
468
-
469
- for cidx, complex_idx in enumerate(complex_indices):
470
- if cidx + 1 >= len(pnames):
471
- continue
472
- amp = _analytical_param_value(shift_param_values, name_to_idx, pnames[cidx + 1])
473
- stoich = float(eq_mat[comp_idx, complex_idx])
474
- frac = np.zeros_like(denom, dtype=float)
475
- np.divide(
476
- stoich * spec_calc[:, complex_idx],
477
- denom,
478
- out=frac,
479
- where=np.isfinite(denom) & (np.abs(denom) > 0),
480
- )
481
- out[:, obs_idx] += amp * frac
482
-
483
- return out
484
-
485
-
486
- def calc_analytical_linear_observables(
487
- spec_calc: np.ndarray,
488
- linear_param_values: np.ndarray,
489
- linear_param_names: list,
490
- linear_obs_param_map: list,
491
- ) -> np.ndarray:
492
- """Compute concentration-weighted linear observables for the analytical fast-exchange path.
493
-
494
- Used for UV-vis (Beer-Lambert) and fluorescence: A_i = sum_j eps_{ij} * [species_j].
495
-
496
- Args:
497
- spec_calc: species concentrations, shape (n_pts, n_species).
498
- linear_param_values: flat array of current non-binding parameter values.
499
- linear_param_names: list of param names corresponding to linear_param_values.
500
- linear_obs_param_map: list of lists; outer index = observable column, inner index = species.
501
- Each inner entry is a param name string or None (dark/silent species).
502
-
503
- Returns:
504
- ndarray of shape (n_pts, n_obs).
505
- """
506
- n_obs = len(linear_obs_param_map)
507
- out = np.zeros((spec_calc.shape[0], n_obs), dtype=float)
508
- name_to_idx = {name: i for i, name in enumerate(linear_param_names)}
509
-
510
- for obs_idx, pnames in enumerate(linear_obs_param_map):
511
- for species_idx, pname in enumerate(pnames):
512
- if not pname:
513
- continue # dark / silent species
514
- coeff = _analytical_param_value(linear_param_values, name_to_idx, pname)
515
- out[:, obs_idx] += coeff * spec_calc[:, species_idx]
516
-
517
- return out
518
-
519
-
520
- def fitfun_analytical_fast_exchange(params, fcn_opts):
521
- if isinstance(params, lmfit.parameter.Parameters):
522
- parvals = list(params.valuesdict().values())
523
- else:
524
- parvals = params
525
-
526
- nK = int(fcn_opts["nK"])
527
- binding_params = np.array([*parvals][:nK], dtype=np.float64)
528
- comp_concs = np.array(fcn_opts["compConcs"], dtype=float)
529
- eq_mat = np.array(fcn_opts["eqMat"], dtype=float)
530
- topology = str(fcn_opts.get("analytical_topology", ""))
531
- complex_indices = [int(x) for x in fcn_opts.get("analytical_complex_indices", [])]
532
- obs_components = [int(x) for x in fcn_opts.get("analytical_obs_components", [])]
533
- obs_param_map = list(fcn_opts.get("analytical_obs_param_map", []))
534
- linear_obs_param_map = list(fcn_opts.get("analytical_linear_obs_param_map", []))
535
-
536
- spec_calc, error = calc_analytical_speciation(
537
- comp_concs=comp_concs,
538
- eq_mat=eq_mat,
539
- binding_params=binding_params,
540
- topology=topology,
541
- n_comp=int(comp_concs.shape[1]),
542
- complex_indices=complex_indices,
543
- )
544
-
545
- if fcn_opts["optTarget"] == "concs":
546
- if fcn_opts["ret"] == "residual":
547
- res = (fcn_opts["exptData"] - spec_calc) / fcn_opts["sigma"]
548
- if error and fcn_opts["mcmc"] is True:
549
- return np.nan
550
- if error:
551
- res = res * 10
552
- return res
553
- if fcn_opts["ret"] == "concs":
554
- return spec_calc
555
- return -1
556
-
557
- shift_params = np.array([*parvals][nK:], dtype=np.float64)
558
- shift_param_names = fcn_opts["paramNames"][nK:]
559
-
560
- # Route to linear (UV-vis / fluorescence) or NMR-shift observable calculation.
561
- if linear_obs_param_map:
562
- obs_calc = calc_analytical_linear_observables(
563
- spec_calc=spec_calc,
564
- linear_param_values=shift_params,
565
- linear_param_names=shift_param_names,
566
- linear_obs_param_map=linear_obs_param_map,
567
- )
568
- else:
569
- obs_calc = calc_analytical_observables(
570
- spec_calc=spec_calc,
571
- comp_concs=comp_concs,
572
- eq_mat=eq_mat,
573
- obs_components=obs_components,
574
- complex_indices=complex_indices,
575
- shift_param_values=shift_params,
576
- shift_param_names=shift_param_names,
577
- obs_param_map=obs_param_map,
578
- )
579
-
580
- if fcn_opts["ret"] == "residual":
581
- res = (fcn_opts["exptData"] - obs_calc) / fcn_opts["sigma"]
582
- nan_mask = np.isnan(fcn_opts["exptData"])
583
- res[nan_mask] = 0
584
- if error and fcn_opts["mcmc"] is True:
585
- return np.nan
586
- if error:
587
- res = res * 10
588
- if fcn_opts["mcmc"] is True:
589
- res = np.nan_to_num(res)
590
- return res
591
- if fcn_opts["ret"] == "concs":
592
- return obs_calc
593
- return -1
594
-
595
-
596
429
  def fitfun(params, fcn_opts): # ,eqMat,specConcs,startShifts=None,sigma=1,ret='residual'):
597
430
 
598
431
  # fcn_opts = {'compConcs': compConcs,
@@ -604,12 +437,7 @@ def fitfun(params, fcn_opts): # ,eqMat,specConcs,startShifts=None,sigma=1,ret='
604
437
  # 'sigma': 1,
605
438
  # 'ret': 'residual'}
606
439
 
607
- if fcn_opts.get("analytical_fast_exchange") is True and fcn_opts.get("analytical_topology") in (
608
- "1:1",
609
- "1:2",
610
- "2:1",
611
- ):
612
- return fitfun_analytical_fast_exchange(params, fcn_opts)
440
+
613
441
 
614
442
  if isinstance(params, lmfit.parameter.Parameters):
615
443
  parvals = list(params.valuesdict().values())
@@ -727,12 +555,12 @@ def concToDelta(concs, specToDd, shiftParams, paramNames):
727
555
  specToDdTrial = specToDdTrial.astype(np.float64)
728
556
 
729
557
  # normalize concs to mol fractions
730
- truthMat = np.array(specToDdTrial, dtype=bool)
558
+ truthMat = ~np.isnan(specToDdTrial)
731
559
  shiftCalc = []
732
560
  for cc in concs:
733
561
  tt = (truthMat.T * cc).T
734
562
  moleFracs = tt / tt.sum(axis=0)
735
- sc = (moleFracs * specToDdTrial).sum(axis=0)
563
+ sc = np.nansum(moleFracs * specToDdTrial, axis=0)
736
564
  shiftCalc.append(sc)
737
565
 
738
566
  return shiftCalc
@@ -924,12 +752,7 @@ class bindingModel:
924
752
  self.specToLinear: Optional[np.ndarray] = None # (n_species, n_obs) object array for UV-vis / fluorescence
925
753
  self.analytical_fast_exchange: bool = False
926
754
  self.analytical_topology: Optional[str] = None
927
- self.analytical_obs_columns: list[str] = []
928
- self.analytical_obs_components: list[int] = []
929
755
  self.analytical_complex_indices: list[int] = []
930
- self.analytical_obs_param_map: list[list[str]] = []
931
- self.analytical_linear_obs_columns: list[str] = []
932
- self.analytical_linear_obs_param_map: list[list] = [] # per-obs list of param names (or None) in species order
933
756
 
934
757
  self.params = lmfit.Parameters()
935
758
  self.mini = None
@@ -1012,91 +835,7 @@ class bindingModel:
1012
835
  if topology_res is not None:
1013
836
  self.analytical_topology, self.analytical_complex_indices = topology_res
1014
837
 
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 = []
838
+
1100
839
 
1101
840
  for paramName in self.compNames:
1102
841
  self._addParam("log" + paramName, init=0, vary=False)
@@ -1111,22 +850,7 @@ class bindingModel:
1111
850
  if isinstance(x, lmfit.Parameter):
1112
851
  self._addExistingParam(x)
1113
852
 
1114
- if self.analytical_fast_exchange:
1115
- n_complex = len(self.analytical_complex_indices)
1116
- if n_complex == 0:
1117
- n_complex = 1
1118
- self.analytical_obs_param_map = []
1119
- for col_name in self.analytical_obs_columns:
1120
- token = _analytical_obs_param_token(col_name)
1121
- pname_list = [f"delta0_{token}"]
1122
- self._addParam(pname_list[0], init=0.0, min=-1000, max=1000, vary=True)
1123
- for cidx in range(n_complex):
1124
- pname = f"deltac{cidx + 1}_{token}"
1125
- pname_list.append(pname)
1126
- self._addParam(pname, init=0.0, min=-1000, max=1000, vary=True)
1127
- self.analytical_obs_param_map.append(pname_list)
1128
- # specToLinear params already registered above; no further action needed.
1129
- return
853
+
1130
854
 
1131
855
  # add chemical shift fitting params
1132
856
  if self.specToDd is not None:
@@ -1172,11 +896,7 @@ class bindingModel:
1172
896
  "paramNames": list(self.params.keys()),
1173
897
  "analytical_fast_exchange": analytical_mode,
1174
898
  "analytical_topology": self.analytical_topology,
1175
- "analytical_obs_columns": list(self.analytical_obs_columns),
1176
- "analytical_obs_components": list(self.analytical_obs_components),
1177
899
  "analytical_complex_indices": list(self.analytical_complex_indices),
1178
- "analytical_obs_param_map": [list(x) for x in self.analytical_obs_param_map],
1179
- "analytical_linear_obs_param_map": [list(x) for x in self.analytical_linear_obs_param_map],
1180
900
  }
1181
901
 
1182
902
  # save settings for later plots
@@ -1228,10 +948,7 @@ class bindingModel:
1228
948
  "paramNames": list(params.keys()),
1229
949
  "analytical_fast_exchange": bool(self.analytical_fast_exchange),
1230
950
  "analytical_topology": self.analytical_topology,
1231
- "analytical_obs_columns": list(self.analytical_obs_columns),
1232
- "analytical_obs_components": list(self.analytical_obs_components),
1233
951
  "analytical_complex_indices": list(self.analytical_complex_indices),
1234
- "analytical_obs_param_map": [list(x) for x in self.analytical_obs_param_map],
1235
952
  }
1236
953
 
1237
954
  return np.array(fitfun(params, fcn_opts))
File without changes