bindmc 0.1.5__tar.gz → 0.1.7__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.
- {bindmc-0.1.5 → bindmc-0.1.7}/PKG-INFO +13 -3
- {bindmc-0.1.5 → bindmc-0.1.7}/README.md +10 -0
- {bindmc-0.1.5 → bindmc-0.1.7}/pyproject.toml +3 -3
- {bindmc-0.1.5 → bindmc-0.1.7}/src/bindmc/__main__.py +2 -0
- {bindmc-0.1.5 → bindmc-0.1.7}/src/bindmc/main.py +10 -1
- {bindmc-0.1.5 → bindmc-0.1.7}/src/bindmc/webgui/classes/ExptData.py +1 -1
- {bindmc-0.1.5 → bindmc-0.1.7}/src/bindmc/webgui/classes/FitResult.py +0 -4
- {bindmc-0.1.5 → bindmc-0.1.7}/src/bindmc/webgui/classes/MCMCSim.py +10 -3
- {bindmc-0.1.5 → bindmc-0.1.7}/src/bindmc/webgui/components/bayes.py +157 -21
- {bindmc-0.1.5 → bindmc-0.1.7}/src/bindmc/webgui/components/body.py +2 -0
- {bindmc-0.1.5 → bindmc-0.1.7}/src/bindmc/webgui/components/data_model.py +102 -94
- {bindmc-0.1.5 → bindmc-0.1.7}/src/bindmc/webgui/components/fitting.py +31 -109
- {bindmc-0.1.5 → bindmc-0.1.7}/src/bindmc/webgui/export/notebook_exporter.py +32 -19
- {bindmc-0.1.5 → bindmc-0.1.7}/src/bindmc/webgui/state/statemanager.py +10 -17
- {bindmc-0.1.5 → bindmc-0.1.7}/src/bindmc/__init__.py +0 -0
- {bindmc-0.1.5 → bindmc-0.1.7}/src/bindmc/webgui/Class model.md +0 -0
- {bindmc-0.1.5 → bindmc-0.1.7}/src/bindmc/webgui/TODO.md +0 -0
- {bindmc-0.1.5 → bindmc-0.1.7}/src/bindmc/webgui/TODO_old.md +0 -0
- {bindmc-0.1.5 → bindmc-0.1.7}/src/bindmc/webgui/__init__.py +0 -0
- {bindmc-0.1.5 → bindmc-0.1.7}/src/bindmc/webgui/app.py +0 -0
- {bindmc-0.1.5 → bindmc-0.1.7}/src/bindmc/webgui/classes/BindingConstant.py +0 -0
- {bindmc-0.1.5 → bindmc-0.1.7}/src/bindmc/webgui/classes/ChemicalShiftParam.py +0 -0
- {bindmc-0.1.5 → bindmc-0.1.7}/src/bindmc/webgui/classes/Component.py +0 -0
- {bindmc-0.1.5 → bindmc-0.1.7}/src/bindmc/webgui/classes/ExptDataType.py +0 -0
- {bindmc-0.1.5 → bindmc-0.1.7}/src/bindmc/webgui/classes/Model.py +0 -0
- {bindmc-0.1.5 → bindmc-0.1.7}/src/bindmc/webgui/classes/RawData.py +0 -0
- {bindmc-0.1.5 → bindmc-0.1.7}/src/bindmc/webgui/classes/Simulation.py +0 -0
- {bindmc-0.1.5 → bindmc-0.1.7}/src/bindmc/webgui/classes/UIBindings.py +0 -0
- {bindmc-0.1.5 → bindmc-0.1.7}/src/bindmc/webgui/classes/__init__.py +0 -0
- {bindmc-0.1.5 → bindmc-0.1.7}/src/bindmc/webgui/components/__init__.py +0 -0
- {bindmc-0.1.5 → bindmc-0.1.7}/src/bindmc/webgui/components/base.py +0 -0
- {bindmc-0.1.5 → bindmc-0.1.7}/src/bindmc/webgui/components/bayes_priors.py +0 -0
- {bindmc-0.1.5 → bindmc-0.1.7}/src/bindmc/webgui/components/binding_model.py +0 -0
- {bindmc-0.1.5 → bindmc-0.1.7}/src/bindmc/webgui/components/data_gen.py +0 -0
- {bindmc-0.1.5 → bindmc-0.1.7}/src/bindmc/webgui/components/data_import.py +0 -0
- {bindmc-0.1.5 → bindmc-0.1.7}/src/bindmc/webgui/components/graph.py +0 -0
- {bindmc-0.1.5 → bindmc-0.1.7}/src/bindmc/webgui/components/header.py +0 -0
- {bindmc-0.1.5 → bindmc-0.1.7}/src/bindmc/webgui/components/simulation.py +0 -0
- {bindmc-0.1.5 → bindmc-0.1.7}/src/bindmc/webgui/default_models.json +0 -0
- {bindmc-0.1.5 → bindmc-0.1.7}/src/bindmc/webgui/export/__init__.py +0 -0
- {bindmc-0.1.5 → bindmc-0.1.7}/src/bindmc/webgui/state/__init__.py +0 -0
- {bindmc-0.1.5 → bindmc-0.1.7}/src/bindmc/webgui/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: bindmc
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.7
|
|
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
|
|
@@ -12,7 +12,7 @@ Classifier: Programming Language :: Python :: 3.14
|
|
|
12
12
|
Classifier: Topic :: Scientific/Engineering :: Chemistry
|
|
13
13
|
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
|
|
14
14
|
Requires-Dist: arviz==0.21.0
|
|
15
|
-
Requires-Dist: bindtools>=0.
|
|
15
|
+
Requires-Dist: bindtools>=0.2.2
|
|
16
16
|
Requires-Dist: corner==2.2.3
|
|
17
17
|
Requires-Dist: emcee==3.1.6
|
|
18
18
|
Requires-Dist: h5py>=3.14.0
|
|
@@ -22,7 +22,7 @@ Requires-Dist: matplotlib==3.10.7
|
|
|
22
22
|
Requires-Dist: nicegui[plotly]==3.3.1
|
|
23
23
|
Requires-Dist: numba>=0.65.1
|
|
24
24
|
Requires-Dist: numpy>=2.3.5
|
|
25
|
-
Requires-Dist: openpyxl
|
|
25
|
+
Requires-Dist: openpyxl>=3.1.5
|
|
26
26
|
Requires-Dist: pandas>=2.3.3
|
|
27
27
|
Requires-Dist: platformdirs>=3.0.0
|
|
28
28
|
Requires-Dist: pywebview>=5
|
|
@@ -53,6 +53,16 @@ Download the latest executable for your platform from the [Releases](https://git
|
|
|
53
53
|
pip install bindmc
|
|
54
54
|
```
|
|
55
55
|
|
|
56
|
+
## Upgrades
|
|
57
|
+
|
|
58
|
+
### Pre-built Binary
|
|
59
|
+
Re-download from the link above.
|
|
60
|
+
|
|
61
|
+
### Pip
|
|
62
|
+
```bash
|
|
63
|
+
pip install --upgrade bindmc
|
|
64
|
+
```
|
|
65
|
+
|
|
56
66
|
## Usage
|
|
57
67
|
|
|
58
68
|
If using the pre-built binary, run the downloaded executable.
|
|
@@ -15,6 +15,16 @@ Download the latest executable for your platform from the [Releases](https://git
|
|
|
15
15
|
pip install bindmc
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
+
## Upgrades
|
|
19
|
+
|
|
20
|
+
### Pre-built Binary
|
|
21
|
+
Re-download from the link above.
|
|
22
|
+
|
|
23
|
+
### Pip
|
|
24
|
+
```bash
|
|
25
|
+
pip install --upgrade bindmc
|
|
26
|
+
```
|
|
27
|
+
|
|
18
28
|
## Usage
|
|
19
29
|
|
|
20
30
|
If using the pre-built binary, run the downloaded executable.
|
|
@@ -4,7 +4,7 @@ build-backend = "uv_build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "bindmc"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.7"
|
|
8
8
|
readme = "README.md"
|
|
9
9
|
keywords = ["chemistry", "analytical chemistry", "binding constants", "supramolecular"]
|
|
10
10
|
classifiers = [
|
|
@@ -28,7 +28,7 @@ classifiers = [
|
|
|
28
28
|
requires-python = ">=3.12"
|
|
29
29
|
dependencies = [
|
|
30
30
|
"arviz==0.21.0",
|
|
31
|
-
"bindtools>=0.
|
|
31
|
+
"bindtools>=0.2.2",
|
|
32
32
|
"corner==2.2.3",
|
|
33
33
|
"emcee==3.1.6",
|
|
34
34
|
"h5py>=3.14.0",
|
|
@@ -38,7 +38,7 @@ dependencies = [
|
|
|
38
38
|
"nicegui[plotly]==3.3.1",
|
|
39
39
|
"numba>=0.65.1",
|
|
40
40
|
"numpy>=2.3.5",
|
|
41
|
-
"openpyxl
|
|
41
|
+
"openpyxl>=3.1.5",
|
|
42
42
|
"pandas>=2.3.3",
|
|
43
43
|
"platformdirs>=3.0.0",
|
|
44
44
|
"pywebview>=5",
|
|
@@ -18,6 +18,11 @@ from nicegui import native, ui, app
|
|
|
18
18
|
from bindmc.webgui.app import BindMCServer
|
|
19
19
|
import logging
|
|
20
20
|
import nicegui
|
|
21
|
+
import nicegui.binding
|
|
22
|
+
|
|
23
|
+
# Suppress NiceGUI's data binding warning (which confuses users with chemical binding)
|
|
24
|
+
nicegui.binding.MAX_PROPAGATION_TIME = float("inf")
|
|
25
|
+
|
|
21
26
|
from packaging.version import InvalidVersion, Version
|
|
22
27
|
from pathlib import Path
|
|
23
28
|
from platformdirs import user_data_dir
|
|
@@ -107,7 +112,11 @@ storage_path.mkdir(parents=True, exist_ok=True)
|
|
|
107
112
|
# Redirect native window persistence data away from default paths
|
|
108
113
|
app.native.start_args["storage_path"] = str(storage_path)
|
|
109
114
|
|
|
115
|
+
def main() -> None:
|
|
116
|
+
ui.run(title="BindMC", reload=reload, native=native_mode, port=native.find_open_port(), storage_secret="bindmc_secret", reconnect_timeout=300)
|
|
117
|
+
|
|
110
118
|
if __name__ in {"__main__", "__mp_main__"}:
|
|
111
|
-
|
|
119
|
+
main()
|
|
120
|
+
|
|
112
121
|
|
|
113
122
|
|
|
@@ -443,7 +443,7 @@ class ExptData:
|
|
|
443
443
|
species_key = species_names[j]
|
|
444
444
|
parameter_matrix[i, j] = _get_parameter_for_species(species_key, delta_col)
|
|
445
445
|
else:
|
|
446
|
-
parameter_matrix[i, j] =
|
|
446
|
+
parameter_matrix[i, j] = None
|
|
447
447
|
|
|
448
448
|
self.delta_to_spec = parameter_matrix
|
|
449
449
|
return parameter_matrix
|
|
@@ -30,8 +30,6 @@ class FitResult:
|
|
|
30
30
|
bd_model: Optional[bd.bindingModel] = None
|
|
31
31
|
analytical_fast_exchange: bool = False
|
|
32
32
|
analytical_topology: Optional[str] = None
|
|
33
|
-
analytical_obs_columns: list[str] = field(default_factory=list)
|
|
34
|
-
analytical_obs_components: list[int] = field(default_factory=list)
|
|
35
33
|
analytical_complex_indices: list[int] = field(default_factory=list)
|
|
36
34
|
|
|
37
35
|
init_model: InitVar[Optional[Model]] = None # The model used for the fit, if any
|
|
@@ -150,8 +148,6 @@ class FitResult:
|
|
|
150
148
|
"success": self.success,
|
|
151
149
|
"analytical_fast_exchange": self.analytical_fast_exchange,
|
|
152
150
|
"analytical_topology": self.analytical_topology,
|
|
153
|
-
"analytical_obs_columns": list(self.analytical_obs_columns),
|
|
154
|
-
"analytical_obs_components": list(self.analytical_obs_components),
|
|
155
151
|
"analytical_complex_indices": list(self.analytical_complex_indices),
|
|
156
152
|
"fit_speciation": (
|
|
157
153
|
self.fit_speciation.to_dict(orient="list") if isinstance(self.fit_speciation, pd.DataFrame) else {}
|
|
@@ -25,6 +25,7 @@ class MCMCSim:
|
|
|
25
25
|
|
|
26
26
|
burn: int = 100
|
|
27
27
|
thin: int = 1
|
|
28
|
+
max_retained_points: int = 1000
|
|
28
29
|
seed: Optional[int] = None
|
|
29
30
|
chains: np.ndarray = field(default_factory=lambda: np.array([])) # Array to hold the MCMC chains
|
|
30
31
|
priors: list[dict[str, Any]] = field(default_factory=list)
|
|
@@ -198,11 +199,17 @@ class MCMCSim:
|
|
|
198
199
|
logger.info("MCMC run cancelled.")
|
|
199
200
|
return self.mc
|
|
200
201
|
chunk_size = self.chunk_size_val.value
|
|
201
|
-
|
|
202
|
+
remaining_raw = self.nsteps_target - self.nsteps_done
|
|
203
|
+
raw_chunk = min(chunk_size, remaining_raw)
|
|
204
|
+
|
|
205
|
+
# Convert raw chunk size to stored samples chunk size based on thin factor
|
|
206
|
+
samples_stored = max(1, raw_chunk // self.thin)
|
|
207
|
+
actual_raw = samples_stored * self.thin
|
|
208
|
+
|
|
202
209
|
b = io.StringIO()
|
|
203
|
-
self.mc.run(samples=
|
|
210
|
+
self.mc.run(samples=samples_stored, thin=self.thin, pool=pool, tqdm_kwargs={"file": b})
|
|
204
211
|
|
|
205
|
-
self.nsteps_done +=
|
|
212
|
+
self.nsteps_done += actual_raw
|
|
206
213
|
self.q_percent_done.put(self.nsteps_done / self.nsteps_target)
|
|
207
214
|
self.q2_tqdm_out.put(b.getvalue().splitlines()[-1])
|
|
208
215
|
if self.mc.sampler is not None:
|
|
@@ -122,6 +122,10 @@ class BayesPanel(BaseComponent):
|
|
|
122
122
|
"Export to Notebook",
|
|
123
123
|
on_click=self._open_export_dialog,
|
|
124
124
|
).classes("mt-2")
|
|
125
|
+
self.advanced_settings_button = ui.button(
|
|
126
|
+
"Advanced Settings",
|
|
127
|
+
on_click=self._open_advanced_settings_dialog,
|
|
128
|
+
).classes("mt-2")
|
|
125
129
|
|
|
126
130
|
# Control buttons
|
|
127
131
|
with ui.row().classes("mt-4"):
|
|
@@ -162,6 +166,7 @@ class BayesPanel(BaseComponent):
|
|
|
162
166
|
self.is_running = False
|
|
163
167
|
self.should_stop = False
|
|
164
168
|
self.completed_steps = 0
|
|
169
|
+
self.mcmc_max_points = 1000
|
|
165
170
|
self.progress_timer = None
|
|
166
171
|
self.status_timer = None
|
|
167
172
|
self.graph_timer = None
|
|
@@ -205,8 +210,8 @@ class BayesPanel(BaseComponent):
|
|
|
205
210
|
tau = e.tau
|
|
206
211
|
if notify:
|
|
207
212
|
s = (
|
|
208
|
-
f"Autocorrelation time is likely too short. Max tau is {int(np.max(tau))}; "
|
|
209
|
-
f"nsteps is {self.completed_steps}. Re-run for at least {int(50 * np.max(tau))} steps."
|
|
213
|
+
f"Autocorrelation time is likely too short. Max tau is {int(np.max(tau))* self.mcmc.thin}; "
|
|
214
|
+
f"nsteps is {self.completed_steps} (before thinning). Re-run for at least {int(50 * np.max(tau) * self.mcmc.thin)} steps."
|
|
210
215
|
)
|
|
211
216
|
ui.notify(s, type="warning")
|
|
212
217
|
self.result_area.content += f"""
|
|
@@ -244,6 +249,16 @@ class BayesPanel(BaseComponent):
|
|
|
244
249
|
ui.notify("No active fit result.", type="warning")
|
|
245
250
|
return
|
|
246
251
|
|
|
252
|
+
ndim = self.get_ndim()
|
|
253
|
+
min_walkers = 2 * ndim
|
|
254
|
+
nwalkers = int(self.nwalkers_input.value)
|
|
255
|
+
if nwalkers < min_walkers:
|
|
256
|
+
ui.notify(
|
|
257
|
+
f"You need at least {min_walkers} parameters.",
|
|
258
|
+
type="warning",
|
|
259
|
+
)
|
|
260
|
+
return
|
|
261
|
+
|
|
247
262
|
if active_fit.bd_model is None:
|
|
248
263
|
logger.info("No bindtools model selected for fitting, generating one.")
|
|
249
264
|
ui.notify("Running an initial fit using least_sq")
|
|
@@ -262,6 +277,9 @@ class BayesPanel(BaseComponent):
|
|
|
262
277
|
nwalkers = int(self.nwalkers_input.value)
|
|
263
278
|
obslist = self.sm.active_expt_data.get_obs_list(self.sm._expt_dtypes)
|
|
264
279
|
|
|
280
|
+
# Calculate thinning factor dynamically based on max points limit
|
|
281
|
+
thin = max(1, nsteps_target // self.mcmc_max_points)
|
|
282
|
+
|
|
265
283
|
logger.info("Setting up for MCMC run")
|
|
266
284
|
# Create MCMC simulation (not yet registered in state)
|
|
267
285
|
self.mcmc = MCMCSim(
|
|
@@ -270,6 +288,8 @@ class BayesPanel(BaseComponent):
|
|
|
270
288
|
bd_model=active_fit.bd_model,
|
|
271
289
|
nwalkers=nwalkers,
|
|
272
290
|
nsteps_target=nsteps_target,
|
|
291
|
+
thin=thin,
|
|
292
|
+
max_retained_points=self.mcmc_max_points,
|
|
273
293
|
priors=list(self.prior_editor.priors) if self.prior_editor.priors else [],
|
|
274
294
|
)
|
|
275
295
|
self.mcmc.setup(obslist)
|
|
@@ -351,9 +371,11 @@ class BayesPanel(BaseComponent):
|
|
|
351
371
|
self.progress_label.text = f"MCMC Complete! ({total_steps} steps with {nwalkers} walkers)"
|
|
352
372
|
self._log_status("MCMC analysis completed successfully")
|
|
353
373
|
ui.notify("MCMC analysis completed successfully!", type="positive")
|
|
354
|
-
|
|
374
|
+
self._log_status("Generating result figures... this may take a while...")
|
|
355
375
|
# Update results
|
|
356
376
|
self._update_results(self.mcmc)
|
|
377
|
+
self._log_status("Figure generation complete!")
|
|
378
|
+
|
|
357
379
|
|
|
358
380
|
except Exception as e:
|
|
359
381
|
self.progress_label.text = "Analysis failed"
|
|
@@ -414,7 +436,7 @@ class BayesPanel(BaseComponent):
|
|
|
414
436
|
|
|
415
437
|
Analysis completed at {pd.Timestamp.now().strftime("%Y-%m-%d %H:%M:%S")}
|
|
416
438
|
"""
|
|
417
|
-
self._make_result_graphs
|
|
439
|
+
ui.timer(0, self._make_result_graphs, once=True)
|
|
418
440
|
|
|
419
441
|
def _update_graphs(self):
|
|
420
442
|
if hasattr(self, "mcmc"):
|
|
@@ -441,7 +463,10 @@ class BayesPanel(BaseComponent):
|
|
|
441
463
|
for w in walkers_to_plot:
|
|
442
464
|
axs[d].plot(chains[:, w, d], label=f"Dim {d} Walker {w}")
|
|
443
465
|
axs[d].set_title(f"Parameter {d}")
|
|
444
|
-
|
|
466
|
+
if self.mcmc.thin != 1:
|
|
467
|
+
axs[d].set_xlabel(f"Steps (thinning 1-in-{self.mcmc.thin} points)")
|
|
468
|
+
else:
|
|
469
|
+
axs[d].set_xlabel(f"Steps")
|
|
445
470
|
|
|
446
471
|
# Plot acceptance fraction
|
|
447
472
|
axs[-1].bar(range(nwalkers), acceptance_frac)
|
|
@@ -547,8 +572,63 @@ class BayesPanel(BaseComponent):
|
|
|
547
572
|
):
|
|
548
573
|
self.mcmc = found_mcmc
|
|
549
574
|
self._update_results(self.mcmc)
|
|
575
|
+
self.nwalkers_input.set_value(found_mcmc.nwalkers)
|
|
576
|
+
self.nsteps_input.set_value(found_mcmc.nsteps_target)
|
|
577
|
+
self.mcmc_max_points = getattr(found_mcmc, "max_retained_points", 1000)
|
|
578
|
+
ndim = self.get_ndim()
|
|
579
|
+
self.nwalkers_input.min = max(2, 2 * ndim)
|
|
580
|
+
else:
|
|
581
|
+
self.mcmc_max_points = 1000
|
|
582
|
+
self.update_default_walkers()
|
|
583
|
+
|
|
584
|
+
def get_ndim(self) -> int:
|
|
585
|
+
active_fit = self.sm.active_fit_or_none
|
|
586
|
+
if active_fit is None:
|
|
587
|
+
return 0
|
|
588
|
+
|
|
589
|
+
varying_params = 0
|
|
590
|
+
if getattr(active_fit, "params", None):
|
|
591
|
+
varying_params = sum(1 for p in active_fit.params.values() if isinstance(p, dict) and p.get("vary") is True)
|
|
592
|
+
elif active_fit.bd_model is not None:
|
|
593
|
+
if active_fit.bd_model.miniResult is not None:
|
|
594
|
+
varying_params = sum(1 for p in active_fit.bd_model.miniResult.params.values() if p.vary)
|
|
595
|
+
elif active_fit.bd_model.params is not None:
|
|
596
|
+
varying_params = sum(1 for p in active_fit.bd_model.params.values() if p.vary)
|
|
597
|
+
|
|
598
|
+
unique_dtypes = set()
|
|
599
|
+
expt_data = self.sm.active_expt_data_or_none
|
|
600
|
+
if expt_data is not None:
|
|
601
|
+
for col, details in expt_data.col_details.items():
|
|
602
|
+
if details.get("depindep") == "dep":
|
|
603
|
+
dtype_key = details.get("dtype")
|
|
604
|
+
if dtype_key is not None:
|
|
605
|
+
edt = self.sm._expt_dtypes.get(dtype_key)
|
|
606
|
+
if edt is not None:
|
|
607
|
+
unique_dtypes.add(edt.meas)
|
|
608
|
+
|
|
609
|
+
return varying_params + len(unique_dtypes)
|
|
610
|
+
|
|
611
|
+
def update_default_walkers(self) -> None:
|
|
612
|
+
active_fit = self.sm.active_fit_or_none
|
|
613
|
+
if active_fit is None:
|
|
614
|
+
return
|
|
615
|
+
|
|
616
|
+
ndim = self.get_ndim()
|
|
617
|
+
default_walkers = 2 * ndim
|
|
618
|
+
self.nwalkers_input.min = max(2, default_walkers)
|
|
619
|
+
|
|
620
|
+
found_mcmc = self._fit_to_mcmc.get(active_fit.id)
|
|
621
|
+
if (
|
|
622
|
+
found_mcmc is not None
|
|
623
|
+
and getattr(found_mcmc, "mc", None) is not None
|
|
624
|
+
and getattr(found_mcmc.mc, "sampler", None) is not None
|
|
625
|
+
):
|
|
626
|
+
self.nwalkers_input.set_value(found_mcmc.nwalkers)
|
|
627
|
+
else:
|
|
628
|
+
self.nwalkers_input.set_value(default_walkers)
|
|
550
629
|
|
|
551
|
-
|
|
630
|
+
|
|
631
|
+
async def _make_result_graphs(self):
|
|
552
632
|
if self.mcmc.mc.sampler is None:
|
|
553
633
|
ui.notify("No chain available; re-run MCMC", type="negative")
|
|
554
634
|
return
|
|
@@ -556,21 +636,26 @@ class BayesPanel(BaseComponent):
|
|
|
556
636
|
self._apply_chain_container_style(self.result_chains, ndim)
|
|
557
637
|
self._apply_corner_container_style(ndim)
|
|
558
638
|
|
|
559
|
-
|
|
560
|
-
|
|
639
|
+
def _plot_chain_sync(fig, mc, w, h):
|
|
640
|
+
fig.clear()
|
|
641
|
+
self._set_figure_size(fig, w, h)
|
|
642
|
+
mc.plot_chain(fig=fig)
|
|
643
|
+
fig.tight_layout()
|
|
644
|
+
|
|
561
645
|
w, h = self._chain_figsize(ndim)
|
|
562
|
-
|
|
563
|
-
self.mcmc.mc.plot_chain(fig=f)
|
|
564
|
-
f.tight_layout()
|
|
646
|
+
await run.io_bound(_plot_chain_sync, self.result_chains.figure, self.mcmc.mc, w, h)
|
|
565
647
|
self.result_chains.update()
|
|
566
648
|
|
|
567
|
-
f = self.result_corner.figure
|
|
568
|
-
f.clear()
|
|
569
|
-
cw, ch = self._corner_figsize(ndim)
|
|
570
|
-
self._set_figure_size(f, cw, ch)
|
|
571
649
|
burnin = self._get_burnin(notify=True)
|
|
572
|
-
|
|
573
|
-
|
|
650
|
+
|
|
651
|
+
def _plot_corner_sync(fig, mc, burnin, cw, ch):
|
|
652
|
+
fig.clear()
|
|
653
|
+
self._set_figure_size(fig, cw, ch)
|
|
654
|
+
mc.make_corner_fig(burnin=burnin, fig=fig)
|
|
655
|
+
fig.tight_layout()
|
|
656
|
+
|
|
657
|
+
cw, ch = self._corner_figsize(ndim)
|
|
658
|
+
await run.io_bound(_plot_corner_sync, self.result_corner.figure, self.mcmc.mc, burnin, cw, ch)
|
|
574
659
|
self.result_corner.update()
|
|
575
660
|
|
|
576
661
|
async def _download_figure(self, fig, filename: str) -> None:
|
|
@@ -583,13 +668,19 @@ class BayesPanel(BaseComponent):
|
|
|
583
668
|
if not hasattr(self, "mcmc") or self.mcmc.mc is None or self.mcmc.mc.sampler is None:
|
|
584
669
|
ui.notify("No chain figure available for download.", type="warning")
|
|
585
670
|
return
|
|
671
|
+
ui.notify("Generating chain figure for download...", type="info")
|
|
586
672
|
ndim = int(self.mcmc.mc.sampler.ndim)
|
|
587
673
|
fig = plt.figure()
|
|
588
674
|
w, h = self._chain_figsize(ndim)
|
|
589
675
|
fig.set_dpi(_EXPORT_DPI)
|
|
590
676
|
fig.set_size_inches(w * 1.2, h * 1.2, forward=True)
|
|
591
|
-
|
|
592
|
-
|
|
677
|
+
|
|
678
|
+
def _plot(f, mc):
|
|
679
|
+
mc.plot_chain(fig=f)
|
|
680
|
+
f.tight_layout()
|
|
681
|
+
|
|
682
|
+
await run.io_bound(_plot, fig, self.mcmc.mc)
|
|
683
|
+
|
|
593
684
|
active_fit = self.sm.active_fit_or_none
|
|
594
685
|
stem = active_fit.name if active_fit is not None else "mcmc"
|
|
595
686
|
filename = f"{safe_filename(stem, fallback='mcmc')}_chains.png"
|
|
@@ -600,20 +691,65 @@ class BayesPanel(BaseComponent):
|
|
|
600
691
|
if not hasattr(self, "mcmc") or self.mcmc.mc is None or self.mcmc.mc.sampler is None:
|
|
601
692
|
ui.notify("No corner figure available for download.", type="warning")
|
|
602
693
|
return
|
|
694
|
+
ui.notify("Generating corner figure for download...", type="info")
|
|
603
695
|
ndim = int(self.mcmc.mc.sampler.ndim)
|
|
604
696
|
fig = plt.figure()
|
|
605
697
|
w, h = self._corner_figsize(ndim)
|
|
606
698
|
fig.set_dpi(_EXPORT_DPI)
|
|
607
699
|
fig.set_size_inches(w * 1.2, h * 1.2, forward=True)
|
|
608
700
|
burnin = self._get_burnin(notify=False)
|
|
609
|
-
|
|
610
|
-
|
|
701
|
+
|
|
702
|
+
def _plot(f, mc, burnin):
|
|
703
|
+
mc.make_corner_fig(burnin=burnin, fig=f)
|
|
704
|
+
f.tight_layout()
|
|
705
|
+
|
|
706
|
+
await run.io_bound(_plot, fig, self.mcmc.mc, burnin)
|
|
707
|
+
|
|
611
708
|
active_fit = self.sm.active_fit_or_none
|
|
612
709
|
stem = active_fit.name if active_fit is not None else "mcmc"
|
|
613
710
|
filename = f"{safe_filename(stem, fallback='mcmc')}_corner.png"
|
|
614
711
|
await self._download_figure(fig, filename)
|
|
615
712
|
plt.close(fig)
|
|
616
713
|
|
|
714
|
+
def _open_advanced_settings_dialog(self) -> None:
|
|
715
|
+
with ui.dialog() as dialog, ui.card().classes("min-w-[24rem] p-6 rounded-lg shadow-lg"):
|
|
716
|
+
ui.label("Advanced MCMC Settings").classes("text-lg font-bold mb-2")
|
|
717
|
+
ui.label("Control how MCMC chains are thinned for plotting and export.").classes("text-xs text-gray-500 mb-4")
|
|
718
|
+
|
|
719
|
+
ui.label("Max points to retain (per walker):").classes("text-sm font-semibold mb-1")
|
|
720
|
+
max_points_input = ui.number(value=self.mcmc_max_points, min=10, max=1000000).classes("w-full mb-2")
|
|
721
|
+
|
|
722
|
+
thin_label = ui.label("").classes("text-xs text-gray-500 font-medium bg-gray-50 p-2 rounded w-full")
|
|
723
|
+
|
|
724
|
+
def update_thin_factor():
|
|
725
|
+
try:
|
|
726
|
+
nsteps = int(self.nsteps_input.value)
|
|
727
|
+
max_pts = int(max_points_input.value or 1000)
|
|
728
|
+
thin = max(1, nsteps // max_pts)
|
|
729
|
+
retained = nsteps // thin
|
|
730
|
+
thin_label.text = f"Calculated thinning factor: {thin} (retains {retained} points)"
|
|
731
|
+
except Exception:
|
|
732
|
+
thin_label.text = "Invalid steps or points value"
|
|
733
|
+
|
|
734
|
+
max_points_input.on_value_change(update_thin_factor)
|
|
735
|
+
update_thin_factor() # Initial update
|
|
736
|
+
|
|
737
|
+
async def save_settings():
|
|
738
|
+
self.mcmc_max_points = int(max_points_input.value or 1000)
|
|
739
|
+
# If MCMCSim exists, update its values
|
|
740
|
+
if hasattr(self, "mcmc") and self.mcmc is not None:
|
|
741
|
+
self.mcmc.max_retained_points = self.mcmc_max_points
|
|
742
|
+
self.mcmc.thin = max(1, self.mcmc.nsteps_target // self.mcmc_max_points)
|
|
743
|
+
self.sm.save_to_storage()
|
|
744
|
+
dialog.close()
|
|
745
|
+
ui.notify("Advanced settings saved.", type="positive")
|
|
746
|
+
|
|
747
|
+
with ui.row().classes("mt-6 gap-2 justify-end w-full"):
|
|
748
|
+
ui.button("Cancel", on_click=dialog.close).props("flat")
|
|
749
|
+
ui.button("Save", on_click=save_settings).props("color=primary")
|
|
750
|
+
|
|
751
|
+
dialog.open()
|
|
752
|
+
|
|
617
753
|
# ------------------------------------------------------------------
|
|
618
754
|
# Notebook export
|
|
619
755
|
# ------------------------------------------------------------------
|
|
@@ -194,6 +194,8 @@ class Body(BaseComponent):
|
|
|
194
194
|
if e.args == "Data model setup":
|
|
195
195
|
# Ensure the data model is updated when switching to the Data model setup tab
|
|
196
196
|
self.components["data_model"]._populate_blocks()
|
|
197
|
+
if e.args == "MCMC":
|
|
198
|
+
self.components["mcmc"].update_default_walkers()
|
|
197
199
|
if e.args == "Fit Results":
|
|
198
200
|
pass
|
|
199
201
|
# Ensure the fit results graph is updated when switching to the Fit Results tab
|
|
@@ -140,14 +140,15 @@ class DataModelPanel(BaseComponent):
|
|
|
140
140
|
|
|
141
141
|
with self.dataModel_specInteg_block:
|
|
142
142
|
self.spec_integ_inps: dict[str, ui.input] = {}
|
|
143
|
+
self.spec_integ_cbs: dict[str, ui.checkbox] = {}
|
|
143
144
|
for i, spec in enumerate(self.sm.species):
|
|
144
145
|
with ui.row().classes("items-center"):
|
|
145
146
|
ui.label(f"Species conc. [{spec}]_free:")
|
|
146
147
|
self.spec_integ_inps[spec] = ui.input().classes("flex-1").props("clearable")
|
|
147
148
|
|
|
148
149
|
self.spec_integ_inps[spec].on("blur", lambda c=self.spec_integ_inps[spec]: self.set_focus(c))
|
|
149
|
-
|
|
150
|
-
self.spec_integ_inps[spec].bind_enabled_from(
|
|
150
|
+
self.spec_integ_cbs[spec] = ui.checkbox("Enabled", value=True).props(f"testid=spec-enabled-{spec}")
|
|
151
|
+
self.spec_integ_inps[spec].bind_enabled_from(self.spec_integ_cbs[spec], "value")
|
|
151
152
|
if (
|
|
152
153
|
hasattr(active_expt, "integ_to_spec")
|
|
153
154
|
and active_expt.integ_to_spec is not None
|
|
@@ -157,9 +158,9 @@ class DataModelPanel(BaseComponent):
|
|
|
157
158
|
active_expt.integ_to_spec[i], active_expt.columns
|
|
158
159
|
)
|
|
159
160
|
if self.spec_integ_inps[spec].value == "":
|
|
160
|
-
|
|
161
|
+
self.spec_integ_cbs[spec].value = False
|
|
161
162
|
else:
|
|
162
|
-
|
|
163
|
+
self.spec_integ_cbs[spec].value = False
|
|
163
164
|
|
|
164
165
|
def _gen_spec_fast_exchange_block(self):
|
|
165
166
|
"""Generate the species fast exchange block.
|
|
@@ -227,95 +228,94 @@ class DataModelPanel(BaseComponent):
|
|
|
227
228
|
self.fast_ex_chem_shift_params = []
|
|
228
229
|
# map per spec-delta index -> { species_name: {card, shift_num, fixed_cb, min_num, max_num} }
|
|
229
230
|
self.fast_ex_chem_shift_map = []
|
|
231
|
+
self.specDeltaCbs = []
|
|
230
232
|
|
|
231
233
|
# keep the fast-exchange column names for later processing and bindings
|
|
232
234
|
self.fast_ex_list_names = list(fast_ex_list)
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
#
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
self.
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
text,
|
|
263
|
-
color="teal",
|
|
264
|
-
on_click=lambda h=text: self._insert_species_into_fast_inp(h),
|
|
265
|
-
)
|
|
266
|
-
# placeholder checkbox to enable the input
|
|
267
|
-
en_cb = ui.checkbox("Enabled", value=True)
|
|
268
|
-
inp.bind_enabled_from(en_cb, "value")
|
|
269
|
-
# placeholder param block element (initially empty/hidden)
|
|
270
|
-
pb = ui.element()
|
|
271
|
-
self.fast_ex_chem_shift_blocks.append(pb)
|
|
272
|
-
self.fast_ex_chem_shift_map.append({})
|
|
273
|
-
# store param dict for later
|
|
274
|
-
# self.fast_ex_chem_shift_params.append({'shift': None, 'fixed': False, 'min': None, 'max': None})
|
|
275
|
-
|
|
276
|
-
# bind blur handler to create/update parameter block
|
|
277
|
-
# use default args to capture current inp and index
|
|
278
|
-
# allow chips/clicks to insert into this input by remembering last focus
|
|
279
|
-
inp.on("focus", lambda e, widget=inp: self.set_focus(widget))
|
|
280
|
-
inp.on("click", lambda e, widget=inp: self.set_focus(widget))
|
|
281
|
-
# handle blur to create/update parameter sub-blocks
|
|
282
|
-
inp.on("blur", lambda e, idx=i, widget=inp: self._handle_spec_delta_blur(idx, widget))
|
|
283
|
-
# immediate change handler: reparse on every change
|
|
284
|
-
inp.on_value_change(lambda e, idx=i, widget=inp: self._handle_spec_delta_blur(idx, widget))
|
|
285
|
-
|
|
286
|
-
# if there is saved delta_to_spec data, populate the input
|
|
287
|
-
if (
|
|
288
|
-
hasattr(active_expt, "delta_to_spec")
|
|
289
|
-
and active_expt.delta_to_spec is not None
|
|
290
|
-
and len(active_expt.delta_to_spec) > 0
|
|
291
|
-
):
|
|
292
|
-
delta_for_eqn = active_expt.delta_to_spec[i].copy()
|
|
293
|
-
|
|
294
|
-
for ij, el in enumerate(delta_for_eqn):
|
|
295
|
-
if np.isclose(el, 0):
|
|
296
|
-
delta_for_eqn[ij] = 0
|
|
297
|
-
else:
|
|
298
|
-
if (f"{self.sm.species[ij]}_free", shift) in active_expt.limiting_shifts:
|
|
299
|
-
s = active_expt.limiting_shifts[f"{self.sm.species[ij]}_free", shift]
|
|
300
|
-
if s.value:
|
|
301
|
-
delta_for_eqn[ij] = delta_for_eqn[ij] / s.value
|
|
302
|
-
else:
|
|
303
|
-
delta_for_eqn[ij] = 1
|
|
304
|
-
|
|
305
|
-
# self.sm.active_expt_data.limiting_shifts[f'{self.sm.species[i]}_free',shift].value
|
|
306
|
-
|
|
307
|
-
# delta_for_eqn[delta_for_eqn != 0] = 1
|
|
308
|
-
|
|
309
|
-
value = self.vec_to_conc_expression(
|
|
310
|
-
delta_for_eqn, [f"{x}_free" for x in self.sm.species]
|
|
235
|
+
with self.dataModel_specFastExchange_block:
|
|
236
|
+
# species_list = [f'{x}_free' for x in self.sm.species]
|
|
237
|
+
|
|
238
|
+
default_shift_species = None
|
|
239
|
+
if simple_model is not None and len(simple_model[1]) > 0:
|
|
240
|
+
default_shift_species = f"{self.sm.species[simple_model[1][0]]}_free"
|
|
241
|
+
|
|
242
|
+
if default_shift_species is not None:
|
|
243
|
+
ui.label(
|
|
244
|
+
f"Analytical model detected: defaulting fast-exchange expression to [{default_shift_species}] "
|
|
245
|
+
"for this observable. You can edit it if needed."
|
|
246
|
+
).classes("text-xs text-gray-600")
|
|
247
|
+
|
|
248
|
+
for i, shift in enumerate(self.fast_ex_list_names):
|
|
249
|
+
# card per chemical shift column
|
|
250
|
+
card = ui.card().classes("mb-2")
|
|
251
|
+
self.specDeltaCards.append(card)
|
|
252
|
+
with card:
|
|
253
|
+
ui.label(f"Fast exchange shift {i + 1} ({shift})").classes("text-sm font-semibold")
|
|
254
|
+
with ui.row().classes("items-center"):
|
|
255
|
+
inp = ui.input().classes("flex-1").props("clearable")
|
|
256
|
+
self.specDeltaInps.append(inp)
|
|
257
|
+
# species chips row will be added below the input to allow quick insertion
|
|
258
|
+
with ui.row().classes("gap-1 mt-2"):
|
|
259
|
+
for text in [f"{x}_free" for x in self.sm.species]:
|
|
260
|
+
ui.chip(
|
|
261
|
+
text,
|
|
262
|
+
color="teal",
|
|
263
|
+
on_click=lambda h=text: self._insert_species_into_fast_inp(h),
|
|
311
264
|
)
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
265
|
+
# placeholder checkbox to enable the input
|
|
266
|
+
en_cb = ui.checkbox("Enabled", value=True)
|
|
267
|
+
self.specDeltaCbs.append(en_cb)
|
|
268
|
+
inp.bind_enabled_from(en_cb, "value")
|
|
269
|
+
# placeholder param block element (initially empty/hidden)
|
|
270
|
+
pb = ui.element()
|
|
271
|
+
self.fast_ex_chem_shift_blocks.append(pb)
|
|
272
|
+
self.fast_ex_chem_shift_map.append({})
|
|
273
|
+
# store param dict for later
|
|
274
|
+
# self.fast_ex_chem_shift_params.append({'shift': None, 'fixed': False, 'min': None, 'max': None})
|
|
275
|
+
|
|
276
|
+
# bind blur handler to create/update parameter block
|
|
277
|
+
# use default args to capture current inp and index
|
|
278
|
+
# allow chips/clicks to insert into this input by remembering last focus
|
|
279
|
+
inp.on("focus", lambda e, widget=inp: self.set_focus(widget))
|
|
280
|
+
inp.on("click", lambda e, widget=inp: self.set_focus(widget))
|
|
281
|
+
# handle blur to create/update parameter sub-blocks
|
|
282
|
+
inp.on("blur", lambda e, idx=i, widget=inp: self._handle_spec_delta_blur(idx, widget))
|
|
283
|
+
# immediate change handler: reparse on every change
|
|
284
|
+
inp.on_value_change(lambda e, idx=i, widget=inp: self._handle_spec_delta_blur(idx, widget))
|
|
285
|
+
|
|
286
|
+
# if there is saved delta_to_spec data, populate the input
|
|
287
|
+
if (
|
|
288
|
+
hasattr(active_expt, "delta_to_spec")
|
|
289
|
+
and active_expt.delta_to_spec is not None
|
|
290
|
+
and len(active_expt.delta_to_spec) > 0
|
|
291
|
+
):
|
|
292
|
+
delta_for_eqn = active_expt.delta_to_spec[i].copy()
|
|
293
|
+
|
|
294
|
+
for ij, el in enumerate(delta_for_eqn):
|
|
295
|
+
if np.isclose(el, 0):
|
|
296
|
+
delta_for_eqn[ij] = 0
|
|
297
|
+
else:
|
|
298
|
+
if (f"{self.sm.species[ij]}_free", shift) in active_expt.limiting_shifts:
|
|
299
|
+
s = active_expt.limiting_shifts[f"{self.sm.species[ij]}_free", shift]
|
|
300
|
+
if s.value:
|
|
301
|
+
delta_for_eqn[ij] = delta_for_eqn[ij] / s.value
|
|
302
|
+
else:
|
|
303
|
+
delta_for_eqn[ij] = 1
|
|
304
|
+
|
|
305
|
+
# self.sm.active_expt_data.limiting_shifts[f'{self.sm.species[i]}_free',shift].value
|
|
306
|
+
|
|
307
|
+
# delta_for_eqn[delta_for_eqn != 0] = 1
|
|
308
|
+
|
|
309
|
+
value = self.vec_to_conc_expression(
|
|
310
|
+
delta_for_eqn, [f"{x}_free" for x in self.sm.species]
|
|
311
|
+
)
|
|
312
|
+
inp.value = value
|
|
313
|
+
if value == "":
|
|
314
|
+
en_cb.value = False
|
|
315
|
+
elif default_shift_species is not None:
|
|
316
|
+
inp.value = f"[{default_shift_species}]"
|
|
317
|
+
# Build corresponding ChemicalShiftParam widgets immediately.
|
|
318
|
+
self._handle_spec_delta_blur(i, inp)
|
|
319
319
|
|
|
320
320
|
# finished generating fast-exchange block
|
|
321
321
|
|
|
@@ -371,6 +371,14 @@ class DataModelPanel(BaseComponent):
|
|
|
371
371
|
if not isinstance(cs_obj, ChemicalShiftParam):
|
|
372
372
|
# create with safe defaults
|
|
373
373
|
cs_obj = ChemicalShiftParam(species=spec_name, col=col_name, fixed=False)
|
|
374
|
+
try:
|
|
375
|
+
if hasattr(active_expt, "data") and active_expt.data is not None and col_name in active_expt.data.columns:
|
|
376
|
+
first_val = float(active_expt.data[col_name].dropna().iloc[0])
|
|
377
|
+
cs_obj.value = round(first_val, 4)
|
|
378
|
+
cs_obj._min = round(first_val - 1.0, 4)
|
|
379
|
+
cs_obj._max = round(first_val + 1.0, 4)
|
|
380
|
+
except Exception:
|
|
381
|
+
pass
|
|
374
382
|
active_expt.limiting_shifts[k] = cs_obj
|
|
375
383
|
|
|
376
384
|
# reuse if exists
|
|
@@ -463,10 +471,10 @@ class DataModelPanel(BaseComponent):
|
|
|
463
471
|
target.integ_to_spec = None
|
|
464
472
|
else:
|
|
465
473
|
integ_to_spec = [
|
|
466
|
-
self.conc_expression_to_vec(
|
|
467
|
-
if
|
|
474
|
+
self.conc_expression_to_vec(self.spec_integ_inps[spec].value, target.columns)
|
|
475
|
+
if self.spec_integ_cbs[spec].value
|
|
468
476
|
else np.zeros(len(target.columns))
|
|
469
|
-
for
|
|
477
|
+
for spec in self.sm.species
|
|
470
478
|
]
|
|
471
479
|
integ_to_spec = np.array(integ_to_spec)
|
|
472
480
|
if integ_to_spec.size == 0 or np.all(np.isclose(integ_to_spec, 0)):
|
|
@@ -483,9 +491,9 @@ class DataModelPanel(BaseComponent):
|
|
|
483
491
|
if hasattr(self, "specDeltaInps") and isinstance(self.specDeltaInps, list) and len(self.specDeltaInps) > 0:
|
|
484
492
|
species_label_list = [f"{s}_free" for s in self.sm.species]
|
|
485
493
|
for idx, input_widget in enumerate(self.specDeltaInps):
|
|
494
|
+
cb = self.specDeltaCbs[idx]
|
|
486
495
|
if (
|
|
487
|
-
|
|
488
|
-
and input_widget.enabled
|
|
496
|
+
cb.value
|
|
489
497
|
and isinstance(input_widget.value, str)
|
|
490
498
|
and input_widget.value.strip()
|
|
491
499
|
):
|
|
@@ -80,6 +80,12 @@ def _prepare_fit_plot_frames(fit: FitResult) -> tuple[pd.DataFrame, pd.DataFrame
|
|
|
80
80
|
|
|
81
81
|
|
|
82
82
|
def _infer_analytical_fast_exchange_config(model, expt_data, expt_dtypes: dict) -> dict[str, object] | None:
|
|
83
|
+
import warnings
|
|
84
|
+
warnings.warn(
|
|
85
|
+
"_infer_analytical_fast_exchange_config is deprecated. Topology detection has been relocated to bindtools.bindingModel.prepModel().",
|
|
86
|
+
DeprecationWarning,
|
|
87
|
+
stacklevel=2,
|
|
88
|
+
)
|
|
83
89
|
if model is None or expt_data is None:
|
|
84
90
|
return None
|
|
85
91
|
|
|
@@ -126,8 +132,6 @@ def _infer_analytical_fast_exchange_config(model, expt_data, expt_dtypes: dict)
|
|
|
126
132
|
return {
|
|
127
133
|
"topology": topo_name,
|
|
128
134
|
"complex_indices": complex_indices,
|
|
129
|
-
"obs_columns": [], # no NMR shift columns
|
|
130
|
-
"obs_components": [],
|
|
131
135
|
}
|
|
132
136
|
|
|
133
137
|
# Pure NMR shift path (existing behaviour).
|
|
@@ -140,85 +144,9 @@ def _infer_analytical_fast_exchange_config(model, expt_data, expt_dtypes: dict)
|
|
|
140
144
|
if finite.size > 0 and np.any(~np.isclose(finite, 0.0)):
|
|
141
145
|
return None
|
|
142
146
|
|
|
143
|
-
component_free_labels = [f"{name}_free" for name in model.component_names]
|
|
144
|
-
if len(component_free_labels) != 2:
|
|
145
|
-
return None
|
|
146
|
-
|
|
147
|
-
# Build optional hints from existing user mappings when available.
|
|
148
|
-
shift_species_by_col: dict[str, set[str]] = {}
|
|
149
|
-
if isinstance(expt_data.limiting_shifts, dict):
|
|
150
|
-
for (species, col_name), _ in expt_data.limiting_shifts.items():
|
|
151
|
-
if col_name is None:
|
|
152
|
-
continue
|
|
153
|
-
shift_species_by_col.setdefault(str(col_name), set()).add(str(species))
|
|
154
|
-
|
|
155
|
-
delta_species_hints: dict[str, set[str]] = {}
|
|
156
|
-
delta_to_spec = expt_data.delta_to_spec
|
|
157
|
-
if isinstance(delta_to_spec, np.ndarray) and delta_to_spec.ndim == 2 and delta_to_spec.size > 0:
|
|
158
|
-
# In this UI flow, delta_to_spec rows are created from fast-exchange observable columns.
|
|
159
|
-
# For analytical mode all dependent observables are shift observables, so the row order
|
|
160
|
-
# corresponds to obs_list.
|
|
161
|
-
n_rows = min(delta_to_spec.shape[0], len(obs_list))
|
|
162
|
-
n_species = min(delta_to_spec.shape[1], len(component_free_labels))
|
|
163
|
-
for ridx in range(n_rows):
|
|
164
|
-
col_name = obs_list[ridx]
|
|
165
|
-
for sidx in range(n_species):
|
|
166
|
-
try:
|
|
167
|
-
is_nonzero = not np.isclose(float(delta_to_spec[ridx, sidx]), 0.0)
|
|
168
|
-
except Exception:
|
|
169
|
-
is_nonzero = bool(delta_to_spec[ridx, sidx])
|
|
170
|
-
if is_nonzero:
|
|
171
|
-
delta_species_hints.setdefault(col_name, set()).add(component_free_labels[sidx])
|
|
172
|
-
|
|
173
|
-
def _component_from_text_hints(col_name: str, dtype_key: str | None) -> int | None:
|
|
174
|
-
# Tokenize to avoid over-matching short component names in arbitrary strings.
|
|
175
|
-
tokens = []
|
|
176
|
-
for src in (col_name, dtype_key or ""):
|
|
177
|
-
parts = [t for t in re.split(r"[^A-Za-z0-9]+", str(src).lower()) if t]
|
|
178
|
-
tokens.extend(parts)
|
|
179
|
-
matches = [idx for idx, comp in enumerate(model.component_names) if str(comp).lower() in tokens]
|
|
180
|
-
return matches[0] if len(matches) == 1 else None
|
|
181
|
-
|
|
182
|
-
obs_components: list[int] = []
|
|
183
|
-
unresolved: list[tuple[int, str]] = []
|
|
184
|
-
for obs_idx, col in enumerate(obs_list):
|
|
185
|
-
col_meta = expt_data.col_details.get(col, {})
|
|
186
|
-
dtype_key = col_meta.get("dtype")
|
|
187
|
-
|
|
188
|
-
included_species = set()
|
|
189
|
-
included_species |= shift_species_by_col.get(col, set())
|
|
190
|
-
included_species |= delta_species_hints.get(col, set())
|
|
191
|
-
|
|
192
|
-
has_comp0 = component_free_labels[0] in included_species
|
|
193
|
-
has_comp1 = component_free_labels[1] in included_species
|
|
194
|
-
if has_comp0 != has_comp1:
|
|
195
|
-
obs_components.append(0 if has_comp0 else 1)
|
|
196
|
-
continue
|
|
197
|
-
|
|
198
|
-
inferred = _component_from_text_hints(col, dtype_key)
|
|
199
|
-
if inferred is not None:
|
|
200
|
-
obs_components.append(inferred)
|
|
201
|
-
continue
|
|
202
|
-
|
|
203
|
-
unresolved.append((obs_idx, col))
|
|
204
|
-
obs_components.append(-1)
|
|
205
|
-
|
|
206
|
-
# Final fallback keeps analytical mode enabled even without manual shift metadata.
|
|
207
|
-
if unresolved:
|
|
208
|
-
for obs_idx, col in unresolved:
|
|
209
|
-
fallback = obs_idx % len(component_free_labels)
|
|
210
|
-
obs_components[obs_idx] = fallback
|
|
211
|
-
logger.info(
|
|
212
|
-
"Analytical fast-exchange: inferred observable '%s' as component %d by default fallback.",
|
|
213
|
-
col,
|
|
214
|
-
fallback,
|
|
215
|
-
)
|
|
216
|
-
|
|
217
147
|
return {
|
|
218
148
|
"topology": topo_name,
|
|
219
149
|
"complex_indices": complex_indices,
|
|
220
|
-
"obs_columns": list(obs_list),
|
|
221
|
-
"obs_components": obs_components,
|
|
222
150
|
}
|
|
223
151
|
|
|
224
152
|
|
|
@@ -229,11 +157,11 @@ class FittingPanel(BaseComponent):
|
|
|
229
157
|
|
|
230
158
|
with self.container:
|
|
231
159
|
ui.label("Fitting panel").classes("text-lg font-bold mb-4")
|
|
232
|
-
with ui.row():
|
|
233
|
-
with ui.card():
|
|
234
|
-
ui.label("Fitting options to go here.")
|
|
160
|
+
with ui.row().classes("w-full gap-4 items-start flex-col lg:flex-row"):
|
|
161
|
+
with ui.card().classes("w-full lg:w-80 shrink-0"):
|
|
162
|
+
# ui.label("Fitting options to go here.")
|
|
235
163
|
self.fit_alg_select = ui.select(
|
|
236
|
-
["least_squares", "l-bfgs", "ampgo"],
|
|
164
|
+
["least_squares", "l-bfgs", "l-bfgs-b", "nelder-mead", "ampgo"],
|
|
237
165
|
label="Algorithm",
|
|
238
166
|
on_change=lambda e: print(f"Selected: {e.value}"),
|
|
239
167
|
value="least_squares",
|
|
@@ -437,16 +365,8 @@ class FittingPanel(BaseComponent):
|
|
|
437
365
|
type="info",
|
|
438
366
|
)
|
|
439
367
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
self.sm.active_expt_data,
|
|
443
|
-
self.sm._expt_dtypes,
|
|
444
|
-
)
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
self.m1 = self.sm.generate_binding_model_for_fit(analytical_cfg=analytical_cfg)
|
|
449
|
-
if analytical_cfg is not None:
|
|
368
|
+
self.m1 = self.sm.generate_binding_model_for_fit()
|
|
369
|
+
if self.m1.analytical_fast_exchange:
|
|
450
370
|
ui.notify(
|
|
451
371
|
f"Using analytical fast-exchange backend ({self.m1.analytical_topology}).",
|
|
452
372
|
type="info",
|
|
@@ -494,18 +414,8 @@ class FittingPanel(BaseComponent):
|
|
|
494
414
|
init_expt_data=self.sm.active_expt_data,
|
|
495
415
|
init_model=self.sm.active_model,
|
|
496
416
|
bd_model=self.m1,
|
|
497
|
-
analytical_fast_exchange=
|
|
417
|
+
analytical_fast_exchange=self.m1.analytical_fast_exchange,
|
|
498
418
|
analytical_topology=self.m1.analytical_topology,
|
|
499
|
-
analytical_obs_columns=(
|
|
500
|
-
[str(x) for x in cast(list[str], analytical_cfg["obs_columns"])]
|
|
501
|
-
if analytical_cfg is not None
|
|
502
|
-
else []
|
|
503
|
-
),
|
|
504
|
-
analytical_obs_components=(
|
|
505
|
-
[int(x) for x in cast(list[int], analytical_cfg["obs_components"])]
|
|
506
|
-
if analytical_cfg is not None
|
|
507
|
-
else []
|
|
508
|
-
),
|
|
509
419
|
analytical_complex_indices=self.m1.analytical_complex_indices,
|
|
510
420
|
)
|
|
511
421
|
self.sm.add_fit(new_fit)
|
|
@@ -520,6 +430,11 @@ class FittingPanel(BaseComponent):
|
|
|
520
430
|
"initial_value": v.init_value,
|
|
521
431
|
}
|
|
522
432
|
|
|
433
|
+
print("DEBUG: specToInteg =", self.sm.active_expt_data.integ_to_spec)
|
|
434
|
+
print("DEBUG: specToDd =", self.sm.active_expt_data.delta_to_spec)
|
|
435
|
+
print("DEBUG: obsList =", self.m1.obsList)
|
|
436
|
+
print("DEBUG: calc_obs shape =", np.shape(calc_obs))
|
|
437
|
+
|
|
523
438
|
self.sm.active_fit.calc_obs = pd.DataFrame(calc_obs, columns=self.m1.obsList)
|
|
524
439
|
|
|
525
440
|
self.sm.active_fit.fit_speciation = pd.DataFrame(speciation, columns=[x + "_free" for x in self.sm.species])
|
|
@@ -718,7 +633,7 @@ class FittingPanel(BaseComponent):
|
|
|
718
633
|
class FitResultsCard(BaseComponent):
|
|
719
634
|
def setup_nicegui(self) -> None:
|
|
720
635
|
|
|
721
|
-
with ui.card():
|
|
636
|
+
with ui.card().classes("w-full lg:flex-1 min-w-0 overflow-hidden"):
|
|
722
637
|
# ui.label("Fitting Results to go here.")
|
|
723
638
|
with ui.row().classes("w-full"):
|
|
724
639
|
ui.label("Results:")
|
|
@@ -785,7 +700,14 @@ class FitResultsCard(BaseComponent):
|
|
|
785
700
|
|
|
786
701
|
fitParams = list(dict.fromkeys(fitParams)) # Remove duplicates
|
|
787
702
|
|
|
788
|
-
paramCols = [
|
|
703
|
+
paramCols = [
|
|
704
|
+
{
|
|
705
|
+
"name": param,
|
|
706
|
+
"label": f"logK({param[3:]})" if param.startswith("log") else param,
|
|
707
|
+
"field": param,
|
|
708
|
+
}
|
|
709
|
+
for param in fitParams
|
|
710
|
+
]
|
|
789
711
|
|
|
790
712
|
stat_col_names = {"chisqr", "aic", "bic", "message", "covariance"}
|
|
791
713
|
stat_cols = [c for c in self.default_columns if c["name"] in stat_col_names]
|
|
@@ -803,7 +725,7 @@ class FitResultsCard(BaseComponent):
|
|
|
803
725
|
for fit in self.sm.fits.values():
|
|
804
726
|
row = {
|
|
805
727
|
"name": fit.name,
|
|
806
|
-
"chisqr": self.rounded_value(fit.chisqr),
|
|
728
|
+
"chisqr": self.rounded_value(fit.chisqr,scientific=True),
|
|
807
729
|
"aic": self.rounded_value(fit.aic, dp=1),
|
|
808
730
|
"bic": self.rounded_value(fit.bic, dp=1),
|
|
809
731
|
"message": fit.termination_message,
|
|
@@ -820,10 +742,10 @@ class FitResultsCard(BaseComponent):
|
|
|
820
742
|
self.add_body_slot()
|
|
821
743
|
self.table.update()
|
|
822
744
|
|
|
823
|
-
def rounded_value(self, value: float, dp: int = 3) -> str:
|
|
745
|
+
def rounded_value(self, value: float, dp: int = 3, scientific=False) -> str:
|
|
824
746
|
"""Round the value for display."""
|
|
825
|
-
if abs(value) >= 10000 or abs(value) < 0.001:
|
|
826
|
-
return f"{value:.
|
|
747
|
+
if abs(value) >= 10000 or abs(value) < 0.001 or scientific:
|
|
748
|
+
return f"{value:.3e}"
|
|
827
749
|
else:
|
|
828
750
|
return f"{value:.{dp}f}" if isinstance(value, float) else str(value)
|
|
829
751
|
|
|
@@ -239,17 +239,7 @@ def _build_lin_obs_cell4_lines(
|
|
|
239
239
|
return lines
|
|
240
240
|
|
|
241
241
|
|
|
242
|
-
def _build_analytical_lin_obs_lines(lin_obs_param_map: list) -> list[str]:
|
|
243
|
-
"""Return setup lines for the analytical path's linear_obs_param_map.
|
|
244
242
|
|
|
245
|
-
Extracts the param name (or None) from each cell so
|
|
246
|
-
fitfun_analytical_fast_exchange can call calc_analytical_linear_observables.
|
|
247
|
-
"""
|
|
248
|
-
# Serialise as [[name_or_none, ...], ...]
|
|
249
|
-
name_map = [[cell["name"] if cell is not None else None for cell in row] for row in lin_obs_param_map]
|
|
250
|
-
return [
|
|
251
|
-
f"m.analytical_linear_obs_param_map = {name_map!r}",
|
|
252
|
-
]
|
|
253
243
|
|
|
254
244
|
|
|
255
245
|
def export_fit_notebook(
|
|
@@ -382,7 +372,31 @@ def export_fit_notebook(
|
|
|
382
372
|
spec_to_integ_arg = "None"
|
|
383
373
|
|
|
384
374
|
if has_delta:
|
|
385
|
-
|
|
375
|
+
# Build spec_to_dd reconstruction lines dynamically to properly instantiate lmfit.Parameters
|
|
376
|
+
spec_to_dd_lines = [
|
|
377
|
+
"spec_to_dd = np.empty((len(species_names), len(obs_list)), dtype=object)"
|
|
378
|
+
]
|
|
379
|
+
delta_T = delta_to_spec.T
|
|
380
|
+
for i in range(delta_T.shape[0]):
|
|
381
|
+
for j in range(delta_T.shape[1]):
|
|
382
|
+
cell = delta_T[i, j]
|
|
383
|
+
if cell is None:
|
|
384
|
+
spec_to_dd_lines.append(f"spec_to_dd[{i}, {j}] = None")
|
|
385
|
+
elif isinstance(cell, (int, float, np.floating)):
|
|
386
|
+
if np.isnan(cell):
|
|
387
|
+
spec_to_dd_lines.append(f"spec_to_dd[{i}, {j}] = None")
|
|
388
|
+
else:
|
|
389
|
+
spec_to_dd_lines.append(f"spec_to_dd[{i}, {j}] = {float(cell)!r}")
|
|
390
|
+
else: # lmfit.Parameter
|
|
391
|
+
pname = getattr(cell, "name", "")
|
|
392
|
+
pval = getattr(cell, "value", 0.0)
|
|
393
|
+
pmin = getattr(cell, "min", -np.inf)
|
|
394
|
+
pmax = getattr(cell, "max", np.inf)
|
|
395
|
+
pvary = getattr(cell, "vary", True)
|
|
396
|
+
spec_to_dd_lines.append(
|
|
397
|
+
f"spec_to_dd[{i}, {j}] = lmfit.Parameter({pname!r}, value={pval!r}, min={pmin!r}, max={pmax!r}, vary={pvary!r})"
|
|
398
|
+
)
|
|
399
|
+
spec_to_dd_line = "\n".join(spec_to_dd_lines)
|
|
386
400
|
spec_to_dd_arg = "spec_to_dd"
|
|
387
401
|
else:
|
|
388
402
|
spec_to_dd_line = "spec_to_dd = None"
|
|
@@ -390,8 +404,6 @@ def export_fit_notebook(
|
|
|
390
404
|
|
|
391
405
|
is_analytical = bool(getattr(fit, "analytical_fast_exchange", False))
|
|
392
406
|
analytical_topology = getattr(fit, "analytical_topology", None)
|
|
393
|
-
analytical_obs_columns = list(getattr(fit, "analytical_obs_columns", []))
|
|
394
|
-
analytical_obs_components = list(getattr(fit, "analytical_obs_components", []))
|
|
395
407
|
analytical_complex_indices = list(getattr(fit, "analytical_complex_indices", []))
|
|
396
408
|
|
|
397
409
|
has_lin_obs = bool(lin_obs_col_names and lin_obs_param_map)
|
|
@@ -399,15 +411,11 @@ def export_fit_notebook(
|
|
|
399
411
|
if is_analytical:
|
|
400
412
|
analytical_setup_lines: list[str] = [
|
|
401
413
|
"",
|
|
402
|
-
"# Analytical fast-exchange
|
|
414
|
+
"# Analytical fast-exchange speciation",
|
|
403
415
|
"m.analytical_fast_exchange = True",
|
|
404
416
|
f"m.analytical_topology = {analytical_topology!r}",
|
|
405
|
-
f"m.analytical_obs_columns = {analytical_obs_columns!r}",
|
|
406
|
-
f"m.analytical_obs_components = {analytical_obs_components!r}",
|
|
407
417
|
f"m.analytical_complex_indices = {analytical_complex_indices!r}",
|
|
408
418
|
]
|
|
409
|
-
if has_lin_obs:
|
|
410
|
-
analytical_setup_lines += _build_analytical_lin_obs_lines(lin_obs_param_map) # type: ignore[arg-type]
|
|
411
419
|
else:
|
|
412
420
|
analytical_setup_lines = []
|
|
413
421
|
|
|
@@ -659,6 +667,7 @@ def export_mcmc_notebook(
|
|
|
659
667
|
f"with h5py.File('{stem}_chains.hdf', 'r') as f:",
|
|
660
668
|
" chain = f['mcmc/chain'][:] # (nsteps, nwalkers, ndim)",
|
|
661
669
|
" log_prob = f['mcmc/log_prob'][:] # (nsteps, nwalkers)",
|
|
670
|
+
" thin = f['mcmc'].attrs.get('thin', 1)",
|
|
662
671
|
"",
|
|
663
672
|
"param_labels = [p for p in m.params if m.params[p].vary]",
|
|
664
673
|
"ndim = chain.shape[2]",
|
|
@@ -671,7 +680,10 @@ def export_mcmc_notebook(
|
|
|
671
680
|
" axes[i].set_ylabel(label)",
|
|
672
681
|
"axes[-1].plot(log_prob, alpha=0.3, lw=0.5, color='k')",
|
|
673
682
|
"axes[-1].set_ylabel('log prob')",
|
|
674
|
-
"
|
|
683
|
+
"if thin != 1:",
|
|
684
|
+
" axes[-1].set_xlabel(f'step (thinning 1-in-{thin} points)')",
|
|
685
|
+
"else:",
|
|
686
|
+
" axes[-1].set_xlabel('step')",
|
|
675
687
|
"fig.tight_layout()",
|
|
676
688
|
"plt.show()",
|
|
677
689
|
"",
|
|
@@ -738,6 +750,7 @@ def export_mcmc_notebook(
|
|
|
738
750
|
if has_blobs:
|
|
739
751
|
g.create_dataset("blobs", data=backend.blobs)
|
|
740
752
|
g.attrs["iteration"] = backend.iteration
|
|
753
|
+
g.attrs["thin"] = thin
|
|
741
754
|
hf.flush()
|
|
742
755
|
h5_bytes = bytes(hf.id.get_file_image())
|
|
743
756
|
|
|
@@ -1111,6 +1111,7 @@ class StateManager:
|
|
|
1111
1111
|
"model_id": str(m.model_id) if m.model_id else None,
|
|
1112
1112
|
"expt_data_id": str(m.expt_data_id) if m.expt_data_id else None,
|
|
1113
1113
|
"nsteps_done": m.nsteps_done,
|
|
1114
|
+
"max_retained_points": m.max_retained_points,
|
|
1114
1115
|
}
|
|
1115
1116
|
for m in self.mcmcs.values()
|
|
1116
1117
|
]
|
|
@@ -1166,6 +1167,9 @@ class StateManager:
|
|
|
1166
1167
|
fittemp = data.get("fits", []) # List of fits
|
|
1167
1168
|
if fittemp:
|
|
1168
1169
|
for fit in fittemp:
|
|
1170
|
+
# backward compatibility to load pre-0.1.7 fits
|
|
1171
|
+
fit.pop("analytical_obs_columns",None)
|
|
1172
|
+
fit.pop("analytical_obs_components",None)
|
|
1169
1173
|
fit_result = FitResult(**fit)
|
|
1170
1174
|
fit_result.fit_speciation = pd.DataFrame(fit.get("fit_speciation", {}))
|
|
1171
1175
|
fit_result.calc_obs = pd.DataFrame(fit.get("calc_obs", {}))
|
|
@@ -1232,7 +1236,7 @@ class StateManager:
|
|
|
1232
1236
|
parameter.vary = bool(cell.get("vary", True))
|
|
1233
1237
|
reconstructed_row.append(parameter)
|
|
1234
1238
|
elif cell is None:
|
|
1235
|
-
reconstructed_row.append(
|
|
1239
|
+
reconstructed_row.append(None)
|
|
1236
1240
|
elif isinstance(cell, (str, int, float)):
|
|
1237
1241
|
try:
|
|
1238
1242
|
reconstructed_row.append(float(cell))
|
|
@@ -1691,7 +1695,10 @@ bd.makeFitResidPlot(fit,plotMask=(0,1),ylabel='Chemical shift (ppm)')"""
|
|
|
1691
1695
|
# return model_str
|
|
1692
1696
|
|
|
1693
1697
|
def generate_binding_model_for_fit(
|
|
1694
|
-
self,
|
|
1698
|
+
self,
|
|
1699
|
+
fit: Optional[FitResult] = None,
|
|
1700
|
+
analytical_cfg: Optional[dict[str, object]] = None,
|
|
1701
|
+
force_numerical: bool = False,
|
|
1695
1702
|
) -> bd.bindingModel:
|
|
1696
1703
|
"""Generate a bindtools.bindingModel from the current active model."""
|
|
1697
1704
|
old_fit = None
|
|
@@ -1732,16 +1739,12 @@ bd.makeFitResidPlot(fit,plotMask=(0,1),ylabel='Chemical shift (ppm)')"""
|
|
|
1732
1739
|
cfg = {
|
|
1733
1740
|
"topology": fit.analytical_topology,
|
|
1734
1741
|
"complex_indices": list(getattr(fit, "analytical_complex_indices", [])),
|
|
1735
|
-
"obs_columns": list(getattr(fit, "analytical_obs_columns", [])),
|
|
1736
|
-
"obs_components": list(getattr(fit, "analytical_obs_components", [])),
|
|
1737
1742
|
}
|
|
1738
1743
|
|
|
1739
1744
|
if cfg is not None:
|
|
1740
1745
|
model.analytical_fast_exchange = True
|
|
1741
1746
|
model.analytical_topology = str(cfg["topology"])
|
|
1742
1747
|
model.analytical_complex_indices = [int(x) for x in cfg["complex_indices"]] # type: ignore[index]
|
|
1743
|
-
model.analytical_obs_columns = [str(x) for x in cfg["obs_columns"]] # type: ignore[index]
|
|
1744
|
-
model.analytical_obs_components = [int(x) for x in cfg["obs_components"]] # type: ignore[index]
|
|
1745
1748
|
logger.info(
|
|
1746
1749
|
"Using analytical fast-exchange backend (%s model) for fitting.",
|
|
1747
1750
|
model.analytical_topology,
|
|
@@ -1777,17 +1780,7 @@ bd.makeFitResidPlot(fit,plotMask=(0,1),ylabel='Chemical shift (ppm)')"""
|
|
|
1777
1780
|
model.analytical_linear_obs_columns = lin_cols
|
|
1778
1781
|
model.analytical_linear_obs_param_map = linear_obs_param_map
|
|
1779
1782
|
|
|
1780
|
-
|
|
1781
|
-
from bindmc.webgui.utils import _infer_simple_fast_exchange_topology
|
|
1782
|
-
topology_res = _infer_simple_fast_exchange_topology(
|
|
1783
|
-
self.active_model.eq_mat, len(self.active_model.component_names)
|
|
1784
|
-
)
|
|
1785
|
-
if topology_res is not None:
|
|
1786
|
-
topo_name, complex_indices = topology_res
|
|
1787
|
-
model.analytical_topology = topo_name
|
|
1788
|
-
model.analytical_complex_indices = complex_indices
|
|
1789
|
-
|
|
1790
|
-
model.prepModel()
|
|
1783
|
+
model.prepModel(force_numerical=force_numerical)
|
|
1791
1784
|
|
|
1792
1785
|
for k in self.active_model.binding_constants:
|
|
1793
1786
|
model.params[f"log{k.species}"].set(
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|