bindmc 0.1.4__tar.gz → 0.1.6__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.4 → bindmc-0.1.6}/PKG-INFO +3 -3
- {bindmc-0.1.4 → bindmc-0.1.6}/pyproject.toml +3 -3
- {bindmc-0.1.4 → bindmc-0.1.6}/src/bindmc/__main__.py +2 -0
- {bindmc-0.1.4 → bindmc-0.1.6}/src/bindmc/main.py +11 -2
- {bindmc-0.1.4 → bindmc-0.1.6}/src/bindmc/webgui/classes/MCMCSim.py +10 -3
- {bindmc-0.1.4 → bindmc-0.1.6}/src/bindmc/webgui/components/bayes.py +164 -23
- {bindmc-0.1.4 → bindmc-0.1.6}/src/bindmc/webgui/components/body.py +2 -0
- {bindmc-0.1.4 → bindmc-0.1.6}/src/bindmc/webgui/components/fitting.py +23 -26
- {bindmc-0.1.4 → bindmc-0.1.6}/src/bindmc/webgui/export/notebook_exporter.py +6 -1
- {bindmc-0.1.4 → bindmc-0.1.6}/src/bindmc/webgui/state/statemanager.py +6 -12
- {bindmc-0.1.4 → bindmc-0.1.6}/README.md +0 -0
- {bindmc-0.1.4 → bindmc-0.1.6}/src/bindmc/__init__.py +0 -0
- {bindmc-0.1.4 → bindmc-0.1.6}/src/bindmc/webgui/Class model.md +0 -0
- {bindmc-0.1.4 → bindmc-0.1.6}/src/bindmc/webgui/TODO.md +0 -0
- {bindmc-0.1.4 → bindmc-0.1.6}/src/bindmc/webgui/TODO_old.md +0 -0
- {bindmc-0.1.4 → bindmc-0.1.6}/src/bindmc/webgui/__init__.py +0 -0
- {bindmc-0.1.4 → bindmc-0.1.6}/src/bindmc/webgui/app.py +0 -0
- {bindmc-0.1.4 → bindmc-0.1.6}/src/bindmc/webgui/classes/BindingConstant.py +0 -0
- {bindmc-0.1.4 → bindmc-0.1.6}/src/bindmc/webgui/classes/ChemicalShiftParam.py +0 -0
- {bindmc-0.1.4 → bindmc-0.1.6}/src/bindmc/webgui/classes/Component.py +0 -0
- {bindmc-0.1.4 → bindmc-0.1.6}/src/bindmc/webgui/classes/ExptData.py +0 -0
- {bindmc-0.1.4 → bindmc-0.1.6}/src/bindmc/webgui/classes/ExptDataType.py +0 -0
- {bindmc-0.1.4 → bindmc-0.1.6}/src/bindmc/webgui/classes/FitResult.py +0 -0
- {bindmc-0.1.4 → bindmc-0.1.6}/src/bindmc/webgui/classes/Model.py +0 -0
- {bindmc-0.1.4 → bindmc-0.1.6}/src/bindmc/webgui/classes/RawData.py +0 -0
- {bindmc-0.1.4 → bindmc-0.1.6}/src/bindmc/webgui/classes/Simulation.py +0 -0
- {bindmc-0.1.4 → bindmc-0.1.6}/src/bindmc/webgui/classes/UIBindings.py +0 -0
- {bindmc-0.1.4 → bindmc-0.1.6}/src/bindmc/webgui/classes/__init__.py +0 -0
- {bindmc-0.1.4 → bindmc-0.1.6}/src/bindmc/webgui/components/__init__.py +0 -0
- {bindmc-0.1.4 → bindmc-0.1.6}/src/bindmc/webgui/components/base.py +0 -0
- {bindmc-0.1.4 → bindmc-0.1.6}/src/bindmc/webgui/components/bayes_priors.py +0 -0
- {bindmc-0.1.4 → bindmc-0.1.6}/src/bindmc/webgui/components/binding_model.py +0 -0
- {bindmc-0.1.4 → bindmc-0.1.6}/src/bindmc/webgui/components/data_gen.py +0 -0
- {bindmc-0.1.4 → bindmc-0.1.6}/src/bindmc/webgui/components/data_import.py +0 -0
- {bindmc-0.1.4 → bindmc-0.1.6}/src/bindmc/webgui/components/data_model.py +0 -0
- {bindmc-0.1.4 → bindmc-0.1.6}/src/bindmc/webgui/components/graph.py +0 -0
- {bindmc-0.1.4 → bindmc-0.1.6}/src/bindmc/webgui/components/header.py +0 -0
- {bindmc-0.1.4 → bindmc-0.1.6}/src/bindmc/webgui/components/simulation.py +0 -0
- {bindmc-0.1.4 → bindmc-0.1.6}/src/bindmc/webgui/default_models.json +0 -0
- {bindmc-0.1.4 → bindmc-0.1.6}/src/bindmc/webgui/export/__init__.py +0 -0
- {bindmc-0.1.4 → bindmc-0.1.6}/src/bindmc/webgui/state/__init__.py +0 -0
- {bindmc-0.1.4 → bindmc-0.1.6}/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.6
|
|
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.1
|
|
15
|
+
Requires-Dist: bindtools>=0.2.1
|
|
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
|
|
@@ -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.6"
|
|
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.1
|
|
31
|
+
"bindtools>=0.2.1",
|
|
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
|
|
|
110
|
-
|
|
111
|
-
ui.run(title="BindMC", reload=reload, native=native_mode, port=native.find_open_port(), storage_secret="bindmc_secret")
|
|
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
|
+
|
|
118
|
+
if __name__ in {"__main__", "__mp_main__"}:
|
|
119
|
+
main()
|
|
120
|
+
|
|
112
121
|
|
|
113
122
|
|
|
@@ -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,8 +249,18 @@ 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")
|
|
250
265
|
m1 = self.sm.generate_binding_model_for_fit(active_fit)
|
|
251
266
|
m1 = await run.cpu_bound(
|
|
@@ -262,6 +277,10 @@ 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
|
+
|
|
283
|
+
logger.info("Setting up for MCMC run")
|
|
265
284
|
# Create MCMC simulation (not yet registered in state)
|
|
266
285
|
self.mcmc = MCMCSim(
|
|
267
286
|
model=self.sm.active_model,
|
|
@@ -269,6 +288,8 @@ class BayesPanel(BaseComponent):
|
|
|
269
288
|
bd_model=active_fit.bd_model,
|
|
270
289
|
nwalkers=nwalkers,
|
|
271
290
|
nsteps_target=nsteps_target,
|
|
291
|
+
thin=thin,
|
|
292
|
+
max_retained_points=self.mcmc_max_points,
|
|
272
293
|
priors=list(self.prior_editor.priors) if self.prior_editor.priors else [],
|
|
273
294
|
)
|
|
274
295
|
self.mcmc.setup(obslist)
|
|
@@ -281,12 +302,13 @@ class BayesPanel(BaseComponent):
|
|
|
281
302
|
type="info",
|
|
282
303
|
timeout=60000,
|
|
283
304
|
)
|
|
305
|
+
logger.info("Running a trial run")
|
|
284
306
|
try:
|
|
285
307
|
trial_elapsed = await run.cpu_bound(partial(_run_mcmc_trial, self.mcmc.mc, _TRIAL_STEPS))
|
|
286
308
|
it_s = _TRIAL_STEPS / trial_elapsed
|
|
287
309
|
full_seconds = nsteps_target * trial_elapsed / _TRIAL_STEPS
|
|
288
310
|
full_time_str = _format_duration(full_seconds)
|
|
289
|
-
|
|
311
|
+
logger.info("Trial run finished; took {trial_elapsed:.1f} s ({it_s:.2f} it/s).")
|
|
290
312
|
with ui.dialog() as timing_dialog, ui.card().classes("w-[min(560px,92vw)]"):
|
|
291
313
|
ui.label("Runtime estimate").classes("text-lg font-bold")
|
|
292
314
|
ui.label(f"{_TRIAL_STEPS:,} steps took {trial_elapsed:.1f} s ({it_s:.2f} it/s).").classes(
|
|
@@ -296,6 +318,8 @@ class BayesPanel(BaseComponent):
|
|
|
296
318
|
f"The full run ({nsteps_target:,} steps, {nwalkers} walkers) "
|
|
297
319
|
f"will take approximately {full_time_str}."
|
|
298
320
|
).classes("mt-2")
|
|
321
|
+
logger.info(f"The full run ({nsteps_target:,} steps, {nwalkers} walkers) "
|
|
322
|
+
f"will take approximately {full_time_str}.")
|
|
299
323
|
ui.label("Do you want to continue?").classes("mt-1 font-medium")
|
|
300
324
|
with ui.row().classes("w-full justify-end gap-2 mt-3"):
|
|
301
325
|
ui.button("Cancel", on_click=lambda: timing_dialog.submit(False))
|
|
@@ -320,6 +344,7 @@ class BayesPanel(BaseComponent):
|
|
|
320
344
|
self.run_button.set_enabled(False)
|
|
321
345
|
self.stop_button.set_enabled(True)
|
|
322
346
|
self.progress_bar.value = 0
|
|
347
|
+
logger.info("Starting MCMC analysis")
|
|
323
348
|
self.progress_label.text = "Starting MCMC analysis..."
|
|
324
349
|
self._log_status("Starting MCMC analysis...")
|
|
325
350
|
self._start_run_timers()
|
|
@@ -346,9 +371,11 @@ class BayesPanel(BaseComponent):
|
|
|
346
371
|
self.progress_label.text = f"MCMC Complete! ({total_steps} steps with {nwalkers} walkers)"
|
|
347
372
|
self._log_status("MCMC analysis completed successfully")
|
|
348
373
|
ui.notify("MCMC analysis completed successfully!", type="positive")
|
|
349
|
-
|
|
374
|
+
self._log_status("Generating result figures... this may take a while...")
|
|
350
375
|
# Update results
|
|
351
376
|
self._update_results(self.mcmc)
|
|
377
|
+
self._log_status("Figure generation complete!")
|
|
378
|
+
|
|
352
379
|
|
|
353
380
|
except Exception as e:
|
|
354
381
|
self.progress_label.text = "Analysis failed"
|
|
@@ -409,7 +436,7 @@ class BayesPanel(BaseComponent):
|
|
|
409
436
|
|
|
410
437
|
Analysis completed at {pd.Timestamp.now().strftime("%Y-%m-%d %H:%M:%S")}
|
|
411
438
|
"""
|
|
412
|
-
self._make_result_graphs
|
|
439
|
+
ui.timer(0, self._make_result_graphs, once=True)
|
|
413
440
|
|
|
414
441
|
def _update_graphs(self):
|
|
415
442
|
if hasattr(self, "mcmc"):
|
|
@@ -436,7 +463,10 @@ class BayesPanel(BaseComponent):
|
|
|
436
463
|
for w in walkers_to_plot:
|
|
437
464
|
axs[d].plot(chains[:, w, d], label=f"Dim {d} Walker {w}")
|
|
438
465
|
axs[d].set_title(f"Parameter {d}")
|
|
439
|
-
|
|
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")
|
|
440
470
|
|
|
441
471
|
# Plot acceptance fraction
|
|
442
472
|
axs[-1].bar(range(nwalkers), acceptance_frac)
|
|
@@ -542,8 +572,63 @@ class BayesPanel(BaseComponent):
|
|
|
542
572
|
):
|
|
543
573
|
self.mcmc = found_mcmc
|
|
544
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
|
|
545
615
|
|
|
546
|
-
|
|
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)
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
async def _make_result_graphs(self):
|
|
547
632
|
if self.mcmc.mc.sampler is None:
|
|
548
633
|
ui.notify("No chain available; re-run MCMC", type="negative")
|
|
549
634
|
return
|
|
@@ -551,21 +636,26 @@ class BayesPanel(BaseComponent):
|
|
|
551
636
|
self._apply_chain_container_style(self.result_chains, ndim)
|
|
552
637
|
self._apply_corner_container_style(ndim)
|
|
553
638
|
|
|
554
|
-
|
|
555
|
-
|
|
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
|
+
|
|
556
645
|
w, h = self._chain_figsize(ndim)
|
|
557
|
-
|
|
558
|
-
self.mcmc.mc.plot_chain(fig=f)
|
|
559
|
-
f.tight_layout()
|
|
646
|
+
await run.io_bound(_plot_chain_sync, self.result_chains.figure, self.mcmc.mc, w, h)
|
|
560
647
|
self.result_chains.update()
|
|
561
648
|
|
|
562
|
-
f = self.result_corner.figure
|
|
563
|
-
f.clear()
|
|
564
|
-
cw, ch = self._corner_figsize(ndim)
|
|
565
|
-
self._set_figure_size(f, cw, ch)
|
|
566
649
|
burnin = self._get_burnin(notify=True)
|
|
567
|
-
|
|
568
|
-
|
|
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)
|
|
569
659
|
self.result_corner.update()
|
|
570
660
|
|
|
571
661
|
async def _download_figure(self, fig, filename: str) -> None:
|
|
@@ -578,13 +668,19 @@ class BayesPanel(BaseComponent):
|
|
|
578
668
|
if not hasattr(self, "mcmc") or self.mcmc.mc is None or self.mcmc.mc.sampler is None:
|
|
579
669
|
ui.notify("No chain figure available for download.", type="warning")
|
|
580
670
|
return
|
|
671
|
+
ui.notify("Generating chain figure for download...", type="info")
|
|
581
672
|
ndim = int(self.mcmc.mc.sampler.ndim)
|
|
582
673
|
fig = plt.figure()
|
|
583
674
|
w, h = self._chain_figsize(ndim)
|
|
584
675
|
fig.set_dpi(_EXPORT_DPI)
|
|
585
676
|
fig.set_size_inches(w * 1.2, h * 1.2, forward=True)
|
|
586
|
-
|
|
587
|
-
|
|
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
|
+
|
|
588
684
|
active_fit = self.sm.active_fit_or_none
|
|
589
685
|
stem = active_fit.name if active_fit is not None else "mcmc"
|
|
590
686
|
filename = f"{safe_filename(stem, fallback='mcmc')}_chains.png"
|
|
@@ -595,20 +691,65 @@ class BayesPanel(BaseComponent):
|
|
|
595
691
|
if not hasattr(self, "mcmc") or self.mcmc.mc is None or self.mcmc.mc.sampler is None:
|
|
596
692
|
ui.notify("No corner figure available for download.", type="warning")
|
|
597
693
|
return
|
|
694
|
+
ui.notify("Generating corner figure for download...", type="info")
|
|
598
695
|
ndim = int(self.mcmc.mc.sampler.ndim)
|
|
599
696
|
fig = plt.figure()
|
|
600
697
|
w, h = self._corner_figsize(ndim)
|
|
601
698
|
fig.set_dpi(_EXPORT_DPI)
|
|
602
699
|
fig.set_size_inches(w * 1.2, h * 1.2, forward=True)
|
|
603
700
|
burnin = self._get_burnin(notify=False)
|
|
604
|
-
|
|
605
|
-
|
|
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
|
+
|
|
606
708
|
active_fit = self.sm.active_fit_or_none
|
|
607
709
|
stem = active_fit.name if active_fit is not None else "mcmc"
|
|
608
710
|
filename = f"{safe_filename(stem, fallback='mcmc')}_corner.png"
|
|
609
711
|
await self._download_figure(fig, filename)
|
|
610
712
|
plt.close(fig)
|
|
611
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
|
+
|
|
612
753
|
# ------------------------------------------------------------------
|
|
613
754
|
# Notebook export
|
|
614
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
|
|
@@ -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
|
|
|
@@ -229,8 +235,8 @@ class FittingPanel(BaseComponent):
|
|
|
229
235
|
|
|
230
236
|
with self.container:
|
|
231
237
|
ui.label("Fitting panel").classes("text-lg font-bold mb-4")
|
|
232
|
-
with ui.row():
|
|
233
|
-
with ui.card():
|
|
238
|
+
with ui.row().classes("w-full gap-4 items-start flex-col lg:flex-row"):
|
|
239
|
+
with ui.card().classes("w-full lg:w-80 shrink-0"):
|
|
234
240
|
ui.label("Fitting options to go here.")
|
|
235
241
|
self.fit_alg_select = ui.select(
|
|
236
242
|
["least_squares", "l-bfgs", "ampgo"],
|
|
@@ -437,16 +443,8 @@ class FittingPanel(BaseComponent):
|
|
|
437
443
|
type="info",
|
|
438
444
|
)
|
|
439
445
|
|
|
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:
|
|
446
|
+
self.m1 = self.sm.generate_binding_model_for_fit()
|
|
447
|
+
if self.m1.analytical_fast_exchange:
|
|
450
448
|
ui.notify(
|
|
451
449
|
f"Using analytical fast-exchange backend ({self.m1.analytical_topology}).",
|
|
452
450
|
type="info",
|
|
@@ -494,18 +492,10 @@ class FittingPanel(BaseComponent):
|
|
|
494
492
|
init_expt_data=self.sm.active_expt_data,
|
|
495
493
|
init_model=self.sm.active_model,
|
|
496
494
|
bd_model=self.m1,
|
|
497
|
-
analytical_fast_exchange=
|
|
495
|
+
analytical_fast_exchange=self.m1.analytical_fast_exchange,
|
|
498
496
|
analytical_topology=self.m1.analytical_topology,
|
|
499
|
-
analytical_obs_columns=(
|
|
500
|
-
|
|
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
|
-
),
|
|
497
|
+
analytical_obs_columns=[str(x) for x in self.m1.analytical_obs_columns],
|
|
498
|
+
analytical_obs_components=[int(x) for x in self.m1.analytical_obs_components],
|
|
509
499
|
analytical_complex_indices=self.m1.analytical_complex_indices,
|
|
510
500
|
)
|
|
511
501
|
self.sm.add_fit(new_fit)
|
|
@@ -535,7 +525,7 @@ class FittingPanel(BaseComponent):
|
|
|
535
525
|
|
|
536
526
|
def _update_fit_graphs(self, e=None) -> None:
|
|
537
527
|
"""Update the fit results display."""
|
|
538
|
-
|
|
528
|
+
logger.info("Updating fit results...")
|
|
539
529
|
self.fit_graph.clear_graph(update=False)
|
|
540
530
|
if len(self.sm.fits) > 0:
|
|
541
531
|
self.speciation_graph.clear_graph(update=False)
|
|
@@ -718,7 +708,7 @@ class FittingPanel(BaseComponent):
|
|
|
718
708
|
class FitResultsCard(BaseComponent):
|
|
719
709
|
def setup_nicegui(self) -> None:
|
|
720
710
|
|
|
721
|
-
with ui.card():
|
|
711
|
+
with ui.card().classes("w-full lg:flex-1 min-w-0 overflow-hidden"):
|
|
722
712
|
# ui.label("Fitting Results to go here.")
|
|
723
713
|
with ui.row().classes("w-full"):
|
|
724
714
|
ui.label("Results:")
|
|
@@ -785,7 +775,14 @@ class FitResultsCard(BaseComponent):
|
|
|
785
775
|
|
|
786
776
|
fitParams = list(dict.fromkeys(fitParams)) # Remove duplicates
|
|
787
777
|
|
|
788
|
-
paramCols = [
|
|
778
|
+
paramCols = [
|
|
779
|
+
{
|
|
780
|
+
"name": param,
|
|
781
|
+
"label": f"logK({param[3:]})" if param.startswith("log") else param,
|
|
782
|
+
"field": param,
|
|
783
|
+
}
|
|
784
|
+
for param in fitParams
|
|
785
|
+
]
|
|
789
786
|
|
|
790
787
|
stat_col_names = {"chisqr", "aic", "bic", "message", "covariance"}
|
|
791
788
|
stat_cols = [c for c in self.default_columns if c["name"] in stat_col_names]
|
|
@@ -659,6 +659,7 @@ def export_mcmc_notebook(
|
|
|
659
659
|
f"with h5py.File('{stem}_chains.hdf', 'r') as f:",
|
|
660
660
|
" chain = f['mcmc/chain'][:] # (nsteps, nwalkers, ndim)",
|
|
661
661
|
" log_prob = f['mcmc/log_prob'][:] # (nsteps, nwalkers)",
|
|
662
|
+
" thin = f['mcmc'].attrs.get('thin', 1)",
|
|
662
663
|
"",
|
|
663
664
|
"param_labels = [p for p in m.params if m.params[p].vary]",
|
|
664
665
|
"ndim = chain.shape[2]",
|
|
@@ -671,7 +672,10 @@ def export_mcmc_notebook(
|
|
|
671
672
|
" axes[i].set_ylabel(label)",
|
|
672
673
|
"axes[-1].plot(log_prob, alpha=0.3, lw=0.5, color='k')",
|
|
673
674
|
"axes[-1].set_ylabel('log prob')",
|
|
674
|
-
"
|
|
675
|
+
"if thin != 1:",
|
|
676
|
+
" axes[-1].set_xlabel(f'step (thinning 1-in-{thin} points)')",
|
|
677
|
+
"else:",
|
|
678
|
+
" axes[-1].set_xlabel('step')",
|
|
675
679
|
"fig.tight_layout()",
|
|
676
680
|
"plt.show()",
|
|
677
681
|
"",
|
|
@@ -738,6 +742,7 @@ def export_mcmc_notebook(
|
|
|
738
742
|
if has_blobs:
|
|
739
743
|
g.create_dataset("blobs", data=backend.blobs)
|
|
740
744
|
g.attrs["iteration"] = backend.iteration
|
|
745
|
+
g.attrs["thin"] = thin
|
|
741
746
|
hf.flush()
|
|
742
747
|
h5_bytes = bytes(hf.id.get_file_image())
|
|
743
748
|
|
|
@@ -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
|
]
|
|
@@ -1691,7 +1692,10 @@ bd.makeFitResidPlot(fit,plotMask=(0,1),ylabel='Chemical shift (ppm)')"""
|
|
|
1691
1692
|
# return model_str
|
|
1692
1693
|
|
|
1693
1694
|
def generate_binding_model_for_fit(
|
|
1694
|
-
self,
|
|
1695
|
+
self,
|
|
1696
|
+
fit: Optional[FitResult] = None,
|
|
1697
|
+
analytical_cfg: Optional[dict[str, object]] = None,
|
|
1698
|
+
force_numerical: bool = False,
|
|
1695
1699
|
) -> bd.bindingModel:
|
|
1696
1700
|
"""Generate a bindtools.bindingModel from the current active model."""
|
|
1697
1701
|
old_fit = None
|
|
@@ -1777,17 +1781,7 @@ bd.makeFitResidPlot(fit,plotMask=(0,1),ylabel='Chemical shift (ppm)')"""
|
|
|
1777
1781
|
model.analytical_linear_obs_columns = lin_cols
|
|
1778
1782
|
model.analytical_linear_obs_param_map = linear_obs_param_map
|
|
1779
1783
|
|
|
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()
|
|
1784
|
+
model.prepModel(force_numerical=force_numerical)
|
|
1791
1785
|
|
|
1792
1786
|
for k in self.active_model.binding_constants:
|
|
1793
1787
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|