bindmc 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.
Files changed (37) hide show
  1. bindmc/main.py +67 -0
  2. bindmc/webgui/__init__.py +0 -0
  3. bindmc/webgui/app.py +54 -0
  4. bindmc/webgui/classes/BindingConstant.py +23 -0
  5. bindmc/webgui/classes/ChemicalShiftParam.py +40 -0
  6. bindmc/webgui/classes/Component.py +111 -0
  7. bindmc/webgui/classes/ExptData.py +485 -0
  8. bindmc/webgui/classes/ExptDataType.py +92 -0
  9. bindmc/webgui/classes/FitResult.py +173 -0
  10. bindmc/webgui/classes/MCMCSim.py +232 -0
  11. bindmc/webgui/classes/Model.py +86 -0
  12. bindmc/webgui/classes/RawData.py +36 -0
  13. bindmc/webgui/classes/Simulation.py +104 -0
  14. bindmc/webgui/classes/UIBindings.py +19 -0
  15. bindmc/webgui/classes/__init__.py +28 -0
  16. bindmc/webgui/components/__init__.py +29 -0
  17. bindmc/webgui/components/base.py +24 -0
  18. bindmc/webgui/components/bayes.py +689 -0
  19. bindmc/webgui/components/bayes_priors.py +351 -0
  20. bindmc/webgui/components/binding_model.py +330 -0
  21. bindmc/webgui/components/body.py +276 -0
  22. bindmc/webgui/components/data_gen.py +419 -0
  23. bindmc/webgui/components/data_import.py +450 -0
  24. bindmc/webgui/components/data_model.py +609 -0
  25. bindmc/webgui/components/fitting.py +886 -0
  26. bindmc/webgui/components/graph.py +649 -0
  27. bindmc/webgui/components/header.py +124 -0
  28. bindmc/webgui/components/simulation.py +385 -0
  29. bindmc/webgui/export/__init__.py +0 -0
  30. bindmc/webgui/export/notebook_exporter.py +727 -0
  31. bindmc/webgui/state/__init__.py +1 -0
  32. bindmc/webgui/state/statemanager.py +2043 -0
  33. bindmc/webgui/utils.py +322 -0
  34. bindmc-0.1.0.dist-info/METADATA +22 -0
  35. bindmc-0.1.0.dist-info/RECORD +37 -0
  36. bindmc-0.1.0.dist-info/WHEEL +5 -0
  37. bindmc-0.1.0.dist-info/top_level.txt +1 -0
bindmc/webgui/utils.py ADDED
@@ -0,0 +1,322 @@
1
+ import re
2
+
3
+ import numpy as np
4
+
5
+ def safe_filename(stem: str, fallback: str = "file") -> str:
6
+ """Return a filesystem-friendly filename stem."""
7
+ safe = "".join(ch if (ch.isalnum() or ch in ("-", "_")) else "_" for ch in stem.strip())
8
+ return safe or fallback
9
+
10
+
11
+ def get_components_from_species(species_name: str,coefficient=1) -> list:
12
+ """
13
+ Extract components from a species name.
14
+
15
+ Args:
16
+ species_name (str): The species name to parse, e.g. Ar2B3
17
+ coefficient (int): The coefficient of the species, default is 1 (i.e. 2Ar3 has coefficient 2 and will return [Ar]*6)
18
+
19
+ Returns:
20
+ list: A list of components found in the species name: [Ar,Ar,B,B,B]
21
+ """
22
+ components = []
23
+
24
+ # match = re.match(r'^(\d*)?([A-Za-z][A-Za-z0-9]*)$', term)
25
+ # if not match:
26
+ # raise ValueError(f"Invalid species term format: {term}")
27
+
28
+ # coeff_str, species = match.groups()
29
+ # coefficient = int(coeff_str) if coeff_str else 1
30
+
31
+ pattern = r'([A-Z][a-z]*)(\d*)'
32
+ match = re.findall(pattern, species_name)
33
+ for m in match:
34
+ comp = m[0]
35
+ count_str = m[1]
36
+ count = int(count_str) if count_str else 1
37
+ components.extend([comp] * count * coefficient)
38
+
39
+ return components
40
+
41
+
42
+ def parse_species_term(term):
43
+ """
44
+ Parse a term like '2A' or 'AB2' to extract coefficient and species name.
45
+
46
+ Args:
47
+ term (str): The term to parse, e.g. '2A', 'AB2'.
48
+
49
+ Returns:
50
+ tuple: A tuple containing the coefficient (int) and species name (str).
51
+
52
+ Raises:
53
+ ValueError: If the term is not in the expected format.
54
+ """
55
+ term = term.strip()
56
+ # Match coefficient (optional) followed by species name
57
+ match = re.match(r'^(\d*)?([A-Za-z][A-Za-z0-9]*)$', term)
58
+ if not match:
59
+ raise ValueError(f"Invalid species term format: {term}")
60
+
61
+ coeff_str, species = match.groups()
62
+ coefficient = int(coeff_str) if coeff_str else 1
63
+ return coefficient, species
64
+
65
+
66
+ def parse_species_composition(species_name, components):
67
+ """Parse a species name to determine how many of each component it contains."""
68
+ composition = {comp: 0 for comp in components}
69
+
70
+ # If it's a pure component, return 1 for that component
71
+ if species_name in components:
72
+ composition[species_name] = 1
73
+ return composition
74
+
75
+ # For complex species, parse the composition
76
+ remaining = species_name
77
+
78
+ for component in sorted(components, key=len, reverse=True): # Longer names first
79
+ # Find all occurrences of this component followed by optional digits
80
+ pattern = f'{re.escape(component)}(\\d*)'
81
+ matches = list(re.finditer(pattern, remaining))
82
+
83
+ total_count = 0
84
+ for match in matches:
85
+ count_str = match.group(1)
86
+ count = int(count_str) if count_str else 1
87
+ total_count += count
88
+ # Remove this match from remaining string
89
+ remaining = remaining[:match.start()] + remaining[match.end():]
90
+
91
+ composition[component] = total_count
92
+
93
+ # Check if there are any unrecognized parts
94
+ if remaining.strip():
95
+ raise ValueError(f"Species '{species_name}' contains unrecognized components: '{remaining}'")
96
+
97
+ return composition
98
+
99
+ def eq_mat_from_equation_str_infer_components(eq_str: str) -> tuple[np.ndarray, list[str], list[str]]:
100
+ """
101
+ Convert a naturally-typed representation of equilibrium matricies into a numpy array.
102
+
103
+ This function infers the components from the species names in the equilibrium string.
104
+
105
+ Args:
106
+ eq_str (str): A string representation of the equilibrium matrix, e.g. "A+B<=>AB; A+2B<=>AB2".
107
+
108
+ Returns:
109
+ np.array: A numpy array representing the equilibrium matrix.
110
+ """
111
+ # Extract all components from the equation string
112
+ components = re.findall(r'[A-Z][a-z]*', eq_str)
113
+ # Remove duplicates while preserving order
114
+ components = list(dict.fromkeys(components))
115
+
116
+ if not components:
117
+ raise ValueError("No components found in the equilibrium string")
118
+
119
+ return eqMatFromEqnStr(eq_str, components)
120
+
121
+ def eqMatFromEqnStr(eq_str: str, components: list) -> tuple[np.ndarray, list, list]:
122
+ """
123
+ Convert a naturally-typed representation of equilibrium matricies into a numpy array.
124
+
125
+
126
+ Args ----
127
+ eq_str (str): A string representation of the equilibrium matrix, e.g. "A+B<=>AB; A+2B<=>AB2".
128
+ the equilibrium indicated by <=> or =.
129
+ Returns ----
130
+ np.array: A numpy array representing the equilibrium matrix, e.g. [[1, 0, 1, 1], [0, 1, 1, 2]].
131
+ In this array, each row corresponds to a component, and each column to a species. In
132
+ the equilibrium string above, the pure components are A and B, and at equilibrium there are
133
+ four species: A, B, AB, and AB2.
134
+
135
+ Reading down the columns of the matrix, we see that:
136
+ - The first column corresponds to Afree, which comprises 1 A and 0 B.
137
+ - The second column corresponds to Bfree, which comprises 0 A and 1 B.
138
+ - The third column corresponds to AB, which comprises 1 A and 1 B.
139
+ - The fourth column corresponds to AB2, which comprises 1 A and 2 B.
140
+
141
+ Raises -----
142
+ ValueError: If the input string is not in the expected format.
143
+ ValueError: If the species names are not comprised of component names.
144
+ NotImplementedError: If the left hand side of the equation contains any non-component species.
145
+ """
146
+
147
+ # pre-process into separate equilibria, separated by semicolons or newlines
148
+ eq_str=eq_str.replace('\n',';')
149
+ equations = eq_str.split(';')
150
+ if not equations:
151
+ raise ValueError("No valid reactions found in equilibrium string")
152
+
153
+ # if isinstance(equations, str):
154
+ # equations = [equations]
155
+
156
+ lhs = []
157
+ rhs = []
158
+
159
+ # for each eq
160
+ for eq in equations:
161
+ eq = eq.replace(' ', '') # remove all spaces
162
+ if not eq:
163
+ continue
164
+ if '=' not in eq:
165
+ raise ValueError(f"Invalid equilibrium format, missing '=' or '<=>': {eq}")
166
+ if eq.count('=') > 1:
167
+ raise ValueError(f"Invalid equilibrium format, multiple '=' or '<=>': {eq}")
168
+ # split into lhs and rhs
169
+ if '<=>' in eq:
170
+ left, right = eq.split('<=>', 1)
171
+ else:
172
+ left, right = eq.split('=', 1)
173
+ lhs_terms = left.split('+')
174
+ rhs_terms = right.split('+')
175
+
176
+ lhs.append(lhs_terms)
177
+ rhs.append(rhs_terms)
178
+
179
+ allspecies = set()
180
+ allcomponents = set()
181
+ allspecies_list = [] # to keep track of species in order
182
+ for ii, lhs in enumerate(lhs):
183
+ # if not all(comp in components for comp in lhs):
184
+ # raise NotImplementedError(f"Left-hand side of reaction {ii+1} contains non-component species: {lhs}")
185
+
186
+ lhscomp = []
187
+ rhscomp = []
188
+ rhsspec = []
189
+ for spec in lhs:
190
+ # parse the species term to get the coefficient and species name
191
+ coefficient, speciesname = parse_species_term(spec)
192
+ if speciesname not in components:
193
+ raise NotImplementedError(f"Left-hand side contains non-component species: {speciesname}")
194
+ # get the components from the species name
195
+ lhscomp.extend(get_components_from_species(speciesname, coefficient))
196
+
197
+ for spec in rhs[ii]:
198
+ # parse the species term to get the coefficient and species name
199
+ coefficient, species = parse_species_term(spec)
200
+ rhsspec.append(species)
201
+ components_in_species = get_components_from_species(species, coefficient)
202
+ if not all(comp in components for comp in components_in_species):
203
+ raise ValueError(f"Species '{species}' contains unrecognized components: {components_in_species}")
204
+
205
+ # get the components from the species namet
206
+ rhscomp.extend(components_in_species)
207
+
208
+ # check that lhscomp and rhscomp are the same when ordered
209
+ if sorted(lhscomp) != sorted(rhscomp):
210
+ raise ValueError(f"Left-hand side and right-hand side of reaction {ii+1} do not match: {lhscomp} != {rhscomp}")
211
+
212
+ # # check that the species on rhs are all comprised of components
213
+ # for rr in rhs[ii]:
214
+ # for spec in rr:
215
+ # # components have an "elemental" name, i.e. start with an uppercase letter
216
+ # # followed by zero or more lowercase letters only. Species names can also contain digits.
217
+
218
+ # # extract the components from the species name
219
+
220
+ # components_in_species = re.findall(r'[A-Z][a-z]*', species)
221
+ # if not all(comp in components for comp in components_in_species):
222
+ # raise ValueError(f"Species '{species}' contains unrecognized components: {components_in_species}")
223
+
224
+ # add the species and components to the sets
225
+ allspecies.update(lhscomp+rhsspec)
226
+ allcomponents.update(lhscomp)
227
+ allspecies_list.extend(lhscomp)
228
+ allspecies_list.extend(rhsspec)
229
+
230
+ # create eqMat
231
+ n_components = len(allcomponents)
232
+ n_species = len(allspecies)
233
+ eqMat = np.zeros((n_components, n_species))
234
+
235
+ # make allspecies_list unique:
236
+ allspecies_list = list(dict.fromkeys(allspecies_list)) # preserve order and remove duplicates
237
+
238
+ # make sure all components come first in the allspecies_list
239
+ allspecies_list = [comp for comp in components if comp in allspecies_list] + [spec for spec in allspecies_list if spec not in components]
240
+
241
+ # we can build eqMat directly from the names of the species and components. We do not need to use the user's stoichiometry
242
+ # now that we have checked (above) that their models make chemical sense.
243
+
244
+ for j, species in enumerate(allspecies_list):
245
+ if species in allspecies:
246
+ composition = parse_species_composition(species, components)
247
+ for i, component in enumerate(components):
248
+ eqMat[i, j] = composition[component]
249
+ allspecies.remove(species) # remove species from set to avoid duplicates
250
+
251
+
252
+ return eqMat,components,allspecies_list
253
+
254
+ def eqMatFromStr(eq_str: str) -> np.ndarray:
255
+ """
256
+ Convert a string representation of an equilibrium matrix into a numpy array.
257
+
258
+ Args:
259
+ eq_str (str): A string representation of the equilibrium matrix. e.g. [[1,0,1],[0,1,1]] or [[1 0 1], [0 1 1]]
260
+ or 1, 0, 1\n 0, 1, 1.
261
+
262
+ Returns:
263
+ np.array: A numpy array representing the equilibrium matrix.
264
+ """
265
+ # rows indicated by either newlines, semicolons, or '],' (i.e. closebracket followed by comma)
266
+ # and columns indicated by commas or spaces
267
+
268
+ # here we replace all row indicates with newlines
269
+ eq_str = eq_str.replace(';', '\n').replace('],', '\n')
270
+
271
+ # now remove all brackets
272
+ eq_str = eq_str.replace('[', '').replace(']', '')
273
+
274
+ # now split by newlines
275
+ rows = eq_str.strip().split('\n')
276
+
277
+ # now split each row by commas or spaces
278
+ matrix = []
279
+ for row in rows:
280
+ # Remove any extra whitespace
281
+ row = row.strip()
282
+ if not row:
283
+ continue
284
+ # Split by comma or space
285
+ if ',' in row:
286
+ row_values = [float(x.strip()) for x in row.split(',')]
287
+ else:
288
+ row_values = [float(x) for x in row.split()]
289
+ matrix.append(row_values)
290
+ # Convert to numpy array
291
+ return np.array(matrix, dtype=float)
292
+
293
+
294
+
295
+ def _infer_simple_fast_exchange_topology(eq_mat: np.ndarray, n_comp: int) -> tuple[str, list[int]] | None:
296
+ if not isinstance(eq_mat, np.ndarray) or eq_mat.ndim != 2:
297
+ return None
298
+ if n_comp != 2 or eq_mat.shape[0] != 2 or eq_mat.shape[1] <= n_comp:
299
+ return None
300
+
301
+ bound_indices = list(range(n_comp, eq_mat.shape[1]))
302
+ sig_to_idx: dict[tuple[int, int], int] = {}
303
+ for idx in bound_indices:
304
+ a = eq_mat[0, idx]
305
+ b = eq_mat[1, idx]
306
+ if not np.isfinite(a) or not np.isfinite(b):
307
+ return None
308
+ if not np.isclose(a, round(a)) or not np.isclose(b, round(b)):
309
+ return None
310
+ sig = (int(round(a)), int(round(b)))
311
+ if sig in sig_to_idx:
312
+ return None
313
+ sig_to_idx[sig] = idx
314
+
315
+ sigs = set(sig_to_idx.keys())
316
+ if len(bound_indices) == 1 and sigs == {(1, 1)}:
317
+ return "1:1", [sig_to_idx[(1, 1)]]
318
+ if len(bound_indices) == 2 and sigs == {(1, 1), (1, 2)}:
319
+ return "1:2", [sig_to_idx[(1, 1)], sig_to_idx[(1, 2)]]
320
+ if len(bound_indices) == 2 and sigs == {(1, 1), (2, 1)}:
321
+ return "2:1", [sig_to_idx[(1, 1)], sig_to_idx[(2, 1)]]
322
+ return None
@@ -0,0 +1,22 @@
1
+ Metadata-Version: 2.4
2
+ Name: bindmc
3
+ Version: 0.1.0
4
+ Requires-Python: >=3.12
5
+ Requires-Dist: arviz==0.21.0
6
+ Requires-Dist: bindtools>=0.1.0
7
+ Requires-Dist: corner==2.2.3
8
+ Requires-Dist: emcee==3.1.6
9
+ Requires-Dist: h5py==3.13.0
10
+ Requires-Dist: latex2mathml>=3.0.0
11
+ Requires-Dist: lmfit==1.3.3
12
+ Requires-Dist: matplotlib==3.10.7
13
+ Requires-Dist: nicegui[plotly]==3.3.1
14
+ Requires-Dist: numba>=0.65.1
15
+ Requires-Dist: numpy>=2.3.5
16
+ Requires-Dist: openpyxl==3.1.2
17
+ Requires-Dist: pandas>=2.3.3
18
+ Requires-Dist: platformdirs>=3.0.0
19
+ Requires-Dist: pywebview>=5
20
+ Requires-Dist: scipy==1.16.3
21
+ Requires-Dist: tqdm==4.66.3
22
+ Requires-Dist: uncertainties==3.2.3
@@ -0,0 +1,37 @@
1
+ bindmc/main.py,sha256=zjmu9H0X_WiqNyjHNSWvvBOv4vvt59J8xY5n4DRsWV0,2402
2
+ bindmc/webgui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ bindmc/webgui/app.py,sha256=Fci704HgqZcdEpBBgUIolLSkEllfvAL7q2JbYLjy_FI,1822
4
+ bindmc/webgui/utils.py,sha256=RuTzw7IePBbtjy2CwY1lLm_2BWgs_d67s_Zri61jtCU,12734
5
+ bindmc/webgui/classes/BindingConstant.py,sha256=zNG_2dfqXJIzDwtYC_IVCqjygVi43__Qo3mdMkppYiA,509
6
+ bindmc/webgui/classes/ChemicalShiftParam.py,sha256=GacVWxNWcQZRWa2dZHd6H9-xd0ISokN9VhBUBOjs-vM,1949
7
+ bindmc/webgui/classes/Component.py,sha256=N6cGCZzuV_3CszDEgBpCY9DsCgShhEB-SZNTW-JIDfo,3692
8
+ bindmc/webgui/classes/ExptData.py,sha256=QgVn-pRagRStrnAl22h5Gv4P_RDKXTeAtYExm-St5LI,22888
9
+ bindmc/webgui/classes/ExptDataType.py,sha256=FLCjwXcsThfZiVYSsS-N91tILU2O-JKYci__aSxD2_c,3296
10
+ bindmc/webgui/classes/FitResult.py,sha256=zGmtRyLeXE5A986Nn_syWsO1TyZ6sKtsdmnPln_I1GQ,7263
11
+ bindmc/webgui/classes/MCMCSim.py,sha256=5u1WK65XIm7nnk85yQ_5KkcFT7EvzOf_Qplkx9YkTLM,8747
12
+ bindmc/webgui/classes/Model.py,sha256=TNvwGIB3iOHmV-ZmwETKjSYcHRVndClf9A5py8_5pBs,3142
13
+ bindmc/webgui/classes/RawData.py,sha256=xJlF18Vi1vzJbeVzcmgTuICzlfaqPCD2H6B5rysqfl4,1018
14
+ bindmc/webgui/classes/Simulation.py,sha256=3NkF9q1PXSpq6L-O8XJur7XSYT57xB0iW92T_Ko2xGc,3870
15
+ bindmc/webgui/classes/UIBindings.py,sha256=6tU61RSqIs2WrE0FNmLlbykfbDJpwRZC0aahtZCirOs,467
16
+ bindmc/webgui/classes/__init__.py,sha256=9thl5M9qFfJbBbs9Yb2rAWzLv56syyadUY5URAGXoCA,630
17
+ bindmc/webgui/components/__init__.py,sha256=4YqHWwT2yc5qj58s-q25ewJQnJf-RBkTqRSxSNQJbMo,756
18
+ bindmc/webgui/components/base.py,sha256=Z0e78gQfHEfa9PKJrCFb9z06p_6RX7JS150wt4AL0UI,638
19
+ bindmc/webgui/components/bayes.py,sha256=0iXv-3FmxOMvxtj558rxuSBWiXvSyB44Zm2xhctDcww,30132
20
+ bindmc/webgui/components/bayes_priors.py,sha256=Cahsx0MrLY_PZ_RGYkFsTTK_Tf9L-G5Z9wZpgMeY_9s,17400
21
+ bindmc/webgui/components/binding_model.py,sha256=Hi9OPb75GX4dT9cDlLQcEpGAazBBhQ1K4_GKucaQFus,16241
22
+ bindmc/webgui/components/body.py,sha256=j3l6tHujZU8tIaZ0zCkKtoHVikYl1g7XnGQVkWobgkE,12200
23
+ bindmc/webgui/components/data_gen.py,sha256=A9b46O2K4a1V1NgjsRIFnUPGaWmt5o5U3wwwFwOFdGQ,18595
24
+ bindmc/webgui/components/data_import.py,sha256=yHi3zAY57joClGEyDQ7D04wWX7eYoAdY6bG-ZzDDPVQ,23459
25
+ bindmc/webgui/components/data_model.py,sha256=kLcGGys0gh3otgBr0ryZ6Hs9Rc0hWKL8bHHerYQcasw,30310
26
+ bindmc/webgui/components/fitting.py,sha256=QF0x3GUKtYeoOvsgqM_vTEVGWioa-EzImzmNjkd5oR0,38686
27
+ bindmc/webgui/components/graph.py,sha256=VqvGLGrI8dN496UIU6WCvOEJ_qN_-kBGGAKksJCwEYY,26980
28
+ bindmc/webgui/components/header.py,sha256=Xlv29GWI0AgYWCbcW6Pi_r-GWtYyu9zlnlGSzIuN_tw,5804
29
+ bindmc/webgui/components/simulation.py,sha256=JlRx23lbhG4FUlSWIbdhiZLXoXZXBCL0PM6krmGzWkM,16021
30
+ bindmc/webgui/export/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
31
+ bindmc/webgui/export/notebook_exporter.py,sha256=8GsEpTV-LnQBbqxtNTVvGpqqDYRVLsQ8gB-nWQtDYOU,28127
32
+ bindmc/webgui/state/__init__.py,sha256=kb_7_fc3lIf0k6YXNQMFxBmzPciqfxccVwNogUraXOk,38
33
+ bindmc/webgui/state/statemanager.py,sha256=b7MDe_avUMmkyv-f_6pdwvdVgCDqwZwH1WWqcYl4k7s,88404
34
+ bindmc-0.1.0.dist-info/METADATA,sha256=OxeGMkySTGHBi3VmsopbWSx5vSeZIS3C1BEp4utP8FI,628
35
+ bindmc-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
36
+ bindmc-0.1.0.dist-info/top_level.txt,sha256=GIjspnvAl5ZsXtE7OGlRby7Wkvxr14qGyaonyzfie90,7
37
+ bindmc-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ bindmc