bindmc 0.1.2__tar.gz → 0.1.4__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.2 → bindmc-0.1.4}/PKG-INFO +6 -6
- {bindmc-0.1.2 → bindmc-0.1.4}/README.md +3 -3
- {bindmc-0.1.2 → bindmc-0.1.4}/pyproject.toml +3 -3
- bindmc-0.1.4/src/bindmc/__main__.py +4 -0
- {bindmc-0.1.2 → bindmc-0.1.4}/src/bindmc/main.py +45 -4
- {bindmc-0.1.2 → bindmc-0.1.4}/src/bindmc/webgui/components/bayes.py +14 -13
- {bindmc-0.1.2 → bindmc-0.1.4}/src/bindmc/webgui/components/data_gen.py +5 -1
- {bindmc-0.1.2 → bindmc-0.1.4}/src/bindmc/webgui/components/data_import.py +1 -0
- {bindmc-0.1.2 → bindmc-0.1.4}/src/bindmc/webgui/components/data_model.py +6 -2
- {bindmc-0.1.2 → bindmc-0.1.4}/src/bindmc/webgui/components/fitting.py +11 -12
- {bindmc-0.1.2 → bindmc-0.1.4}/src/bindmc/webgui/components/graph.py +38 -6
- {bindmc-0.1.2 → bindmc-0.1.4}/src/bindmc/webgui/components/simulation.py +6 -6
- {bindmc-0.1.2 → bindmc-0.1.4}/src/bindmc/webgui/state/statemanager.py +12 -2
- {bindmc-0.1.2 → bindmc-0.1.4}/src/bindmc/webgui/utils.py +52 -0
- {bindmc-0.1.2 → bindmc-0.1.4}/src/bindmc/__init__.py +0 -0
- {bindmc-0.1.2 → bindmc-0.1.4}/src/bindmc/webgui/Class model.md +0 -0
- {bindmc-0.1.2 → bindmc-0.1.4}/src/bindmc/webgui/TODO.md +0 -0
- {bindmc-0.1.2 → bindmc-0.1.4}/src/bindmc/webgui/TODO_old.md +0 -0
- {bindmc-0.1.2 → bindmc-0.1.4}/src/bindmc/webgui/__init__.py +0 -0
- {bindmc-0.1.2 → bindmc-0.1.4}/src/bindmc/webgui/app.py +0 -0
- {bindmc-0.1.2 → bindmc-0.1.4}/src/bindmc/webgui/classes/BindingConstant.py +0 -0
- {bindmc-0.1.2 → bindmc-0.1.4}/src/bindmc/webgui/classes/ChemicalShiftParam.py +0 -0
- {bindmc-0.1.2 → bindmc-0.1.4}/src/bindmc/webgui/classes/Component.py +0 -0
- {bindmc-0.1.2 → bindmc-0.1.4}/src/bindmc/webgui/classes/ExptData.py +0 -0
- {bindmc-0.1.2 → bindmc-0.1.4}/src/bindmc/webgui/classes/ExptDataType.py +0 -0
- {bindmc-0.1.2 → bindmc-0.1.4}/src/bindmc/webgui/classes/FitResult.py +0 -0
- {bindmc-0.1.2 → bindmc-0.1.4}/src/bindmc/webgui/classes/MCMCSim.py +0 -0
- {bindmc-0.1.2 → bindmc-0.1.4}/src/bindmc/webgui/classes/Model.py +0 -0
- {bindmc-0.1.2 → bindmc-0.1.4}/src/bindmc/webgui/classes/RawData.py +0 -0
- {bindmc-0.1.2 → bindmc-0.1.4}/src/bindmc/webgui/classes/Simulation.py +0 -0
- {bindmc-0.1.2 → bindmc-0.1.4}/src/bindmc/webgui/classes/UIBindings.py +0 -0
- {bindmc-0.1.2 → bindmc-0.1.4}/src/bindmc/webgui/classes/__init__.py +0 -0
- {bindmc-0.1.2 → bindmc-0.1.4}/src/bindmc/webgui/components/__init__.py +0 -0
- {bindmc-0.1.2 → bindmc-0.1.4}/src/bindmc/webgui/components/base.py +0 -0
- {bindmc-0.1.2 → bindmc-0.1.4}/src/bindmc/webgui/components/bayes_priors.py +0 -0
- {bindmc-0.1.2 → bindmc-0.1.4}/src/bindmc/webgui/components/binding_model.py +0 -0
- {bindmc-0.1.2 → bindmc-0.1.4}/src/bindmc/webgui/components/body.py +0 -0
- {bindmc-0.1.2 → bindmc-0.1.4}/src/bindmc/webgui/components/header.py +0 -0
- {bindmc-0.1.2 → bindmc-0.1.4}/src/bindmc/webgui/default_models.json +0 -0
- {bindmc-0.1.2 → bindmc-0.1.4}/src/bindmc/webgui/export/__init__.py +0 -0
- {bindmc-0.1.2 → bindmc-0.1.4}/src/bindmc/webgui/export/notebook_exporter.py +0 -0
- {bindmc-0.1.2 → bindmc-0.1.4}/src/bindmc/webgui/state/__init__.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.4
|
|
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,10 +12,10 @@ 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.1.3
|
|
16
16
|
Requires-Dist: corner==2.2.3
|
|
17
17
|
Requires-Dist: emcee==3.1.6
|
|
18
|
-
Requires-Dist: h5py
|
|
18
|
+
Requires-Dist: h5py>=3.14.0
|
|
19
19
|
Requires-Dist: latex2mathml>=3.0.0
|
|
20
20
|
Requires-Dist: lmfit==1.3.3
|
|
21
21
|
Requires-Dist: matplotlib==3.10.7
|
|
@@ -38,8 +38,8 @@ Description-Content-Type: text/markdown
|
|
|
38
38
|
|
|
39
39
|
# bindmc
|
|
40
40
|
|
|
41
|
-
[](https://badge.fury.io/py/bindmc)
|
|
42
|
+
[](https://pypi.org/project/bindmc/)
|
|
43
43
|
|
|
44
44
|
`bindmc` is a tool for calculating binding constants, built on top of [bindtools](https://github.com/martinp23/bindtools).
|
|
45
45
|
|
|
@@ -60,4 +60,4 @@ If using the pre-built binary, run the downloaded executable.
|
|
|
60
60
|
If installed via pip, run:
|
|
61
61
|
```bash
|
|
62
62
|
python -m bindmc
|
|
63
|
-
```
|
|
63
|
+
```
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# bindmc
|
|
2
2
|
|
|
3
|
-
[](https://badge.fury.io/py/bindmc)
|
|
4
|
+
[](https://pypi.org/project/bindmc/)
|
|
5
5
|
|
|
6
6
|
`bindmc` is a tool for calculating binding constants, built on top of [bindtools](https://github.com/martinp23/bindtools).
|
|
7
7
|
|
|
@@ -22,4 +22,4 @@ If using the pre-built binary, run the downloaded executable.
|
|
|
22
22
|
If installed via pip, run:
|
|
23
23
|
```bash
|
|
24
24
|
python -m bindmc
|
|
25
|
-
```
|
|
25
|
+
```
|
|
@@ -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.4"
|
|
8
8
|
readme = "README.md"
|
|
9
9
|
keywords = ["chemistry", "analytical chemistry", "binding constants", "supramolecular"]
|
|
10
10
|
classifiers = [
|
|
@@ -28,10 +28,10 @@ classifiers = [
|
|
|
28
28
|
requires-python = ">=3.12"
|
|
29
29
|
dependencies = [
|
|
30
30
|
"arviz==0.21.0",
|
|
31
|
-
"bindtools>=0.1.
|
|
31
|
+
"bindtools>=0.1.3",
|
|
32
32
|
"corner==2.2.3",
|
|
33
33
|
"emcee==3.1.6",
|
|
34
|
-
"h5py
|
|
34
|
+
"h5py>=3.14.0",
|
|
35
35
|
"latex2mathml>=3.0.0",
|
|
36
36
|
"lmfit==1.3.3",
|
|
37
37
|
"matplotlib==3.10.7",
|
|
@@ -13,6 +13,7 @@ import matplotlib
|
|
|
13
13
|
|
|
14
14
|
matplotlib.use("module://matplotlib.backends.backend_svg")
|
|
15
15
|
import sys
|
|
16
|
+
import webview
|
|
16
17
|
from nicegui import native, ui, app
|
|
17
18
|
from bindmc.webgui.app import BindMCServer
|
|
18
19
|
import logging
|
|
@@ -24,6 +25,32 @@ from importlib.metadata import version
|
|
|
24
25
|
|
|
25
26
|
__version__ = version("bindmc")
|
|
26
27
|
|
|
28
|
+
def is_webview_available() -> bool:
|
|
29
|
+
"""Check if pywebview GUI libraries can be initialized in-process."""
|
|
30
|
+
try:
|
|
31
|
+
from webview.guilib import initialize
|
|
32
|
+
from webview.util import WebViewException
|
|
33
|
+
|
|
34
|
+
# Temporarily suppress pywebview's internal logger to prevent
|
|
35
|
+
# missing backend errors from spamming your console output.
|
|
36
|
+
logger = logging.getLogger('pywebview')
|
|
37
|
+
old_level = logger.level
|
|
38
|
+
logger.setLevel(logging.CRITICAL)
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
# Attempts to load the default system GUI engine (e.g. Edge, Cocoa, GTK, QT)
|
|
42
|
+
initialize()
|
|
43
|
+
return True
|
|
44
|
+
except (WebViewException, ImportError, Exception):
|
|
45
|
+
return False
|
|
46
|
+
finally:
|
|
47
|
+
logger.setLevel(old_level)
|
|
48
|
+
|
|
49
|
+
except ImportError:
|
|
50
|
+
# pywebview itself isn't even installed
|
|
51
|
+
return False
|
|
52
|
+
|
|
53
|
+
|
|
27
54
|
logger = logging.getLogger(__name__)
|
|
28
55
|
|
|
29
56
|
try:
|
|
@@ -43,6 +70,8 @@ else:
|
|
|
43
70
|
raise RuntimeError(f"NiceGUI >= 3 is required; found {nicegui.__version__}")
|
|
44
71
|
|
|
45
72
|
|
|
73
|
+
|
|
74
|
+
|
|
46
75
|
app.native.settings["ALLOW_DOWNLOADS"] = True
|
|
47
76
|
|
|
48
77
|
# logging.basicConfig(level=logging.INFO, filename='BindMC.log')
|
|
@@ -51,17 +80,26 @@ logging.basicConfig(level=logging.INFO, format="%(levelname)s:%(message)s")
|
|
|
51
80
|
logger.info(f"Starting BindMC {__version__} NiceGUI server...")
|
|
52
81
|
BindMCServer()
|
|
53
82
|
|
|
54
|
-
#
|
|
55
|
-
|
|
83
|
+
# pyinstaller?
|
|
84
|
+
is_frozen = getattr(sys, "frozen", False)
|
|
85
|
+
|
|
86
|
+
is_module_run = bool(globals().get("__package__"))
|
|
56
87
|
|
|
88
|
+
DEV = not (is_frozen or is_module_run)
|
|
89
|
+
|
|
90
|
+
native_mode = False
|
|
57
91
|
if DEV:
|
|
58
92
|
native_mode = False
|
|
59
93
|
reload = True
|
|
60
94
|
else:
|
|
61
|
-
native_mode =
|
|
95
|
+
native_mode = is_webview_available()
|
|
96
|
+
if native_mode is False:
|
|
97
|
+
logger.warning("Native mode is not available; running via browser.")
|
|
62
98
|
reload = False
|
|
63
99
|
|
|
64
100
|
|
|
101
|
+
|
|
102
|
+
|
|
65
103
|
# make a sensible storage path for native mode
|
|
66
104
|
storage_path = Path(user_data_dir(appname="BindMC", appauthor=False))
|
|
67
105
|
storage_path.mkdir(parents=True, exist_ok=True)
|
|
@@ -69,4 +107,7 @@ storage_path.mkdir(parents=True, exist_ok=True)
|
|
|
69
107
|
# Redirect native window persistence data away from default paths
|
|
70
108
|
app.native.start_args["storage_path"] = str(storage_path)
|
|
71
109
|
|
|
72
|
-
|
|
110
|
+
if __name__ == "__main__":
|
|
111
|
+
ui.run(title="BindMC", reload=reload, native=native_mode, port=native.find_open_port(), storage_secret="bindmc_secret")
|
|
112
|
+
|
|
113
|
+
|
|
@@ -6,7 +6,7 @@ import zipfile
|
|
|
6
6
|
import numpy as np
|
|
7
7
|
import pandas as pd
|
|
8
8
|
from ..classes import MCMCSim
|
|
9
|
-
from ..utils import safe_filename
|
|
9
|
+
from ..utils import safe_filename, custom_download
|
|
10
10
|
from functools import partial
|
|
11
11
|
import asyncio
|
|
12
12
|
import re
|
|
@@ -568,13 +568,13 @@ class BayesPanel(BaseComponent):
|
|
|
568
568
|
f.tight_layout()
|
|
569
569
|
self.result_corner.update()
|
|
570
570
|
|
|
571
|
-
def _download_figure(self, fig, filename: str) -> None:
|
|
571
|
+
async def _download_figure(self, fig, filename: str) -> None:
|
|
572
572
|
buf = io.BytesIO()
|
|
573
573
|
fig.savefig(buf, format="png", dpi=_EXPORT_DPI, bbox_inches="tight")
|
|
574
574
|
buf.seek(0)
|
|
575
|
-
|
|
575
|
+
await custom_download(buf.getvalue(), filename=filename)
|
|
576
576
|
|
|
577
|
-
def download_chain_figure(self) -> None:
|
|
577
|
+
async def download_chain_figure(self) -> None:
|
|
578
578
|
if not hasattr(self, "mcmc") or self.mcmc.mc is None or self.mcmc.mc.sampler is None:
|
|
579
579
|
ui.notify("No chain figure available for download.", type="warning")
|
|
580
580
|
return
|
|
@@ -588,10 +588,10 @@ class BayesPanel(BaseComponent):
|
|
|
588
588
|
active_fit = self.sm.active_fit_or_none
|
|
589
589
|
stem = active_fit.name if active_fit is not None else "mcmc"
|
|
590
590
|
filename = f"{safe_filename(stem, fallback='mcmc')}_chains.png"
|
|
591
|
-
self._download_figure(fig, filename)
|
|
591
|
+
await self._download_figure(fig, filename)
|
|
592
592
|
plt.close(fig)
|
|
593
593
|
|
|
594
|
-
def download_corner_figure(self) -> None:
|
|
594
|
+
async def download_corner_figure(self) -> None:
|
|
595
595
|
if not hasattr(self, "mcmc") or self.mcmc.mc is None or self.mcmc.mc.sampler is None:
|
|
596
596
|
ui.notify("No corner figure available for download.", type="warning")
|
|
597
597
|
return
|
|
@@ -606,7 +606,7 @@ class BayesPanel(BaseComponent):
|
|
|
606
606
|
active_fit = self.sm.active_fit_or_none
|
|
607
607
|
stem = active_fit.name if active_fit is not None else "mcmc"
|
|
608
608
|
filename = f"{safe_filename(stem, fallback='mcmc')}_corner.png"
|
|
609
|
-
self._download_figure(fig, filename)
|
|
609
|
+
await self._download_figure(fig, filename)
|
|
610
610
|
plt.close(fig)
|
|
611
611
|
|
|
612
612
|
# ------------------------------------------------------------------
|
|
@@ -646,19 +646,20 @@ class BayesPanel(BaseComponent):
|
|
|
646
646
|
"text-sm text-orange-600 mt-1"
|
|
647
647
|
)
|
|
648
648
|
|
|
649
|
+
async def export_and_close():
|
|
650
|
+
dialog.close()
|
|
651
|
+
await self._do_export_notebook(include_chains["value"])
|
|
652
|
+
|
|
649
653
|
with ui.row().classes("mt-4 gap-2 justify-end w-full"):
|
|
650
654
|
ui.button("Cancel", on_click=dialog.close)
|
|
651
655
|
ui.button(
|
|
652
656
|
"Export",
|
|
653
|
-
on_click=
|
|
654
|
-
dialog.close(),
|
|
655
|
-
self._do_export_notebook(include_chains["value"]),
|
|
656
|
-
),
|
|
657
|
+
on_click=export_and_close,
|
|
657
658
|
).props("color=primary")
|
|
658
659
|
|
|
659
660
|
dialog.open()
|
|
660
661
|
|
|
661
|
-
def _do_export_notebook(self, include_chains: bool) -> None:
|
|
662
|
+
async def _do_export_notebook(self, include_chains: bool) -> None:
|
|
662
663
|
active_fit = self.sm.active_fit_or_none
|
|
663
664
|
if active_fit is None:
|
|
664
665
|
ui.notify("No active fit to export.", type="negative")
|
|
@@ -684,5 +685,5 @@ class BayesPanel(BaseComponent):
|
|
|
684
685
|
buf.seek(0)
|
|
685
686
|
|
|
686
687
|
zip_filename = f"{stem}_mcmc_notebook.zip"
|
|
687
|
-
|
|
688
|
+
await custom_download(buf.read(), filename=zip_filename)
|
|
688
689
|
ui.notify(f"Notebook exported as {zip_filename}.", type="positive")
|
|
@@ -4,6 +4,7 @@ import numpy as np
|
|
|
4
4
|
import pandas as pd
|
|
5
5
|
from ..classes import Component
|
|
6
6
|
from .graph import Graph
|
|
7
|
+
from ..utils import custom_download
|
|
7
8
|
|
|
8
9
|
from nicegui.events import UploadEventArguments
|
|
9
10
|
from collections import Counter
|
|
@@ -127,9 +128,12 @@ class DataGenerationPanel(BaseComponent):
|
|
|
127
128
|
template_text += "\nmM" + ",mM" * (len(self.sm.components) - 1) # Header row with units
|
|
128
129
|
template_text += "\n" + "0.001" + ",0" * (len(self.sm.components) - 1) # Add a newline after the header
|
|
129
130
|
|
|
131
|
+
async def download_template():
|
|
132
|
+
await custom_download(template_text, "component_concentrations_template.csv")
|
|
133
|
+
|
|
130
134
|
ui.button(
|
|
131
135
|
"Download template CSV",
|
|
132
|
-
on_click=
|
|
136
|
+
on_click=download_template,
|
|
133
137
|
)
|
|
134
138
|
|
|
135
139
|
ui.label("""
|
|
@@ -126,6 +126,7 @@ class DataImportPanel(BaseComponent):
|
|
|
126
126
|
rd = active_raw
|
|
127
127
|
new_expt_data = ExptData(name=rd.filename, init_raw_data=rd, init_model=self.sm.active_model)
|
|
128
128
|
self.sm.add_expt_data(new_expt_data)
|
|
129
|
+
self.sm.notify_listeners("data_imported") # Trigger table and graph update
|
|
129
130
|
else:
|
|
130
131
|
ui.notify("No raw data selected to prepare data model from.", type="negative")
|
|
131
132
|
|
|
@@ -65,6 +65,10 @@ class DataModelPanel(BaseComponent):
|
|
|
65
65
|
|
|
66
66
|
nmr_fast_ex = False
|
|
67
67
|
nmr_slow_ex = False
|
|
68
|
+
|
|
69
|
+
# make all visible to allow changes in the next code block before we hide them again if not needed
|
|
70
|
+
self.dataModel_specInteg_block.visible = True
|
|
71
|
+
self.dataModel_specFastExchange_block.visible = True
|
|
68
72
|
|
|
69
73
|
# work out what we need
|
|
70
74
|
if self.sm.active_expt_data_or_none is not None:
|
|
@@ -76,7 +80,7 @@ class DataModelPanel(BaseComponent):
|
|
|
76
80
|
if getattr(dtype, "meas", None) == "nmr_ppm" and f.get("depindep") == "dep":
|
|
77
81
|
nmr_fast_ex = True
|
|
78
82
|
self._gen_spec_fast_exchange_block()
|
|
79
|
-
elif getattr(dtype, "meas", None) == "
|
|
83
|
+
elif getattr(dtype, "meas", None) == "nmr_integ" and f.get("depindep") == "dep":
|
|
80
84
|
nmr_slow_ex = True
|
|
81
85
|
self._gen_spec_integ_block()
|
|
82
86
|
|
|
@@ -142,7 +146,7 @@ class DataModelPanel(BaseComponent):
|
|
|
142
146
|
self.spec_integ_inps[spec] = ui.input().classes("flex-1").props("clearable")
|
|
143
147
|
|
|
144
148
|
self.spec_integ_inps[spec].on("blur", lambda c=self.spec_integ_inps[spec]: self.set_focus(c))
|
|
145
|
-
b = ui.checkbox("Enabled", value=True)
|
|
149
|
+
b = ui.checkbox("Enabled", value=True).props(f"testid=spec-enabled-{spec}")
|
|
146
150
|
self.spec_integ_inps[spec].bind_enabled_from(b, "value")
|
|
147
151
|
if (
|
|
148
152
|
hasattr(active_expt, "integ_to_spec")
|
|
@@ -12,7 +12,7 @@ import bindtools.binding as bd
|
|
|
12
12
|
from .base import BaseComponent
|
|
13
13
|
from .graph import Graph
|
|
14
14
|
from ..classes import FitResult
|
|
15
|
-
from ..utils import safe_filename, _infer_simple_fast_exchange_topology
|
|
15
|
+
from ..utils import safe_filename, _infer_simple_fast_exchange_topology, custom_download
|
|
16
16
|
from functools import partial
|
|
17
17
|
from typing import cast
|
|
18
18
|
|
|
@@ -108,7 +108,7 @@ def _infer_analytical_fast_exchange_config(model, expt_data, expt_dtypes: dict)
|
|
|
108
108
|
has_nmr = True
|
|
109
109
|
elif meas in ("uvvis", "fluorescence"):
|
|
110
110
|
has_linear = True
|
|
111
|
-
else:
|
|
111
|
+
else:
|
|
112
112
|
return None # Unknown or unsupported observable type for analytical path
|
|
113
113
|
|
|
114
114
|
if has_nmr and has_linear:
|
|
@@ -442,6 +442,9 @@ class FittingPanel(BaseComponent):
|
|
|
442
442
|
self.sm.active_expt_data,
|
|
443
443
|
self.sm._expt_dtypes,
|
|
444
444
|
)
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
|
|
445
448
|
self.m1 = self.sm.generate_binding_model_for_fit(analytical_cfg=analytical_cfg)
|
|
446
449
|
if analytical_cfg is not None:
|
|
447
450
|
ui.notify(
|
|
@@ -492,7 +495,7 @@ class FittingPanel(BaseComponent):
|
|
|
492
495
|
init_model=self.sm.active_model,
|
|
493
496
|
bd_model=self.m1,
|
|
494
497
|
analytical_fast_exchange=analytical_cfg is not None,
|
|
495
|
-
analytical_topology=
|
|
498
|
+
analytical_topology=self.m1.analytical_topology,
|
|
496
499
|
analytical_obs_columns=(
|
|
497
500
|
[str(x) for x in cast(list[str], analytical_cfg["obs_columns"])]
|
|
498
501
|
if analytical_cfg is not None
|
|
@@ -503,11 +506,7 @@ class FittingPanel(BaseComponent):
|
|
|
503
506
|
if analytical_cfg is not None
|
|
504
507
|
else []
|
|
505
508
|
),
|
|
506
|
-
analytical_complex_indices=
|
|
507
|
-
[int(x) for x in cast(list[int], analytical_cfg["complex_indices"])]
|
|
508
|
-
if analytical_cfg is not None
|
|
509
|
-
else []
|
|
510
|
-
),
|
|
509
|
+
analytical_complex_indices=self.m1.analytical_complex_indices,
|
|
511
510
|
)
|
|
512
511
|
self.sm.add_fit(new_fit)
|
|
513
512
|
|
|
@@ -647,7 +646,7 @@ class FittingPanel(BaseComponent):
|
|
|
647
646
|
self.sm.active_fit.name = self.fit_name_input.value
|
|
648
647
|
self.sm.active_fit.description = self.fit_comment_input.value
|
|
649
648
|
|
|
650
|
-
def download_fit_data_csv(self) -> None:
|
|
649
|
+
async def download_fit_data_csv(self) -> None:
|
|
651
650
|
if self.sm.active_fit_id is None:
|
|
652
651
|
ui.notify("No active fit to download.", type="negative")
|
|
653
652
|
return
|
|
@@ -685,10 +684,10 @@ class FittingPanel(BaseComponent):
|
|
|
685
684
|
export_df = pd.concat(export_frames, axis=1)
|
|
686
685
|
filename = f"fit_{safe_filename(fit.name, fallback='fit')}_data.csv"
|
|
687
686
|
csv = export_df.to_csv(index=False, encoding="utf-8", float_format="{:.5e}".format)
|
|
688
|
-
|
|
687
|
+
await custom_download(csv, filename=filename)
|
|
689
688
|
ui.notify(f"Fit data downloaded as {filename}.", type="info")
|
|
690
689
|
|
|
691
|
-
def download_fit_notebook(self) -> None:
|
|
690
|
+
async def download_fit_notebook(self) -> None:
|
|
692
691
|
"""Export the active fit as a zip containing a Jupyter notebook and a data CSV."""
|
|
693
692
|
if self.sm.active_fit_id is None:
|
|
694
693
|
ui.notify("No active fit to export.", type="negative")
|
|
@@ -712,7 +711,7 @@ class FittingPanel(BaseComponent):
|
|
|
712
711
|
buf.seek(0)
|
|
713
712
|
|
|
714
713
|
zip_filename = f"{stem}_notebook.zip"
|
|
715
|
-
|
|
714
|
+
await custom_download(buf.read(), filename=zip_filename)
|
|
716
715
|
ui.notify(f"Notebook exported as {zip_filename}.", type="positive")
|
|
717
716
|
|
|
718
717
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import json
|
|
2
2
|
|
|
3
|
-
from nicegui import ui
|
|
3
|
+
from nicegui import ui, app
|
|
4
4
|
|
|
5
5
|
from .base import BaseComponent
|
|
6
6
|
from ..classes import Simulation, FitResult
|
|
7
|
-
from ..utils import safe_filename
|
|
7
|
+
from ..utils import safe_filename, custom_download
|
|
8
8
|
import pandas as pd
|
|
9
9
|
import uuid
|
|
10
10
|
|
|
@@ -399,7 +399,39 @@ class Graph(BaseComponent):
|
|
|
399
399
|
ui.notify(f"No plotted data available for {filename}.", type="warning")
|
|
400
400
|
return
|
|
401
401
|
|
|
402
|
-
|
|
402
|
+
is_native = False
|
|
403
|
+
try:
|
|
404
|
+
if getattr(app.native, "main_window", None) is not None:
|
|
405
|
+
is_native = True
|
|
406
|
+
except Exception:
|
|
407
|
+
pass
|
|
408
|
+
|
|
409
|
+
if is_native:
|
|
410
|
+
js = f"""
|
|
411
|
+
const root = getElement({self.graph.id});
|
|
412
|
+
const container = root?.$el ?? root;
|
|
413
|
+
const plot = container?.querySelector('.js-plotly-plot') ?? container;
|
|
414
|
+
if (!plot || typeof Plotly === 'undefined') {{
|
|
415
|
+
null;
|
|
416
|
+
}} else {{
|
|
417
|
+
Plotly.toImage(plot, {{format: 'png', scale: 2}});
|
|
418
|
+
}}
|
|
419
|
+
"""
|
|
420
|
+
data_url = await ui.run_javascript(js)
|
|
421
|
+
if not data_url or not data_url.startswith("data:image/png;base64,"):
|
|
422
|
+
ui.notify(f"Unable to export {filename}.png", type="negative")
|
|
423
|
+
return
|
|
424
|
+
|
|
425
|
+
try:
|
|
426
|
+
import base64
|
|
427
|
+
|
|
428
|
+
header, encoded = data_url.split(",", 1)
|
|
429
|
+
image_bytes = base64.b64decode(encoded)
|
|
430
|
+
await custom_download(image_bytes, filename=f"{filename}.png")
|
|
431
|
+
except Exception as e:
|
|
432
|
+
ui.notify(f"Failed to export {filename}.png: {str(e)}", type="negative")
|
|
433
|
+
else:
|
|
434
|
+
js = f"""
|
|
403
435
|
(() => {{
|
|
404
436
|
const root = getElement({self.graph.id});
|
|
405
437
|
const container = root?.$el ?? root;
|
|
@@ -409,9 +441,9 @@ class Graph(BaseComponent):
|
|
|
409
441
|
return true;
|
|
410
442
|
}})()
|
|
411
443
|
"""
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
444
|
+
ok = await ui.run_javascript(js)
|
|
445
|
+
if not ok:
|
|
446
|
+
ui.notify(f"Unable to export {filename}.png", type="negative")
|
|
415
447
|
|
|
416
448
|
def update_plot_compfree(self, e):
|
|
417
449
|
"""Update the plot to show or hide component free concentrations."""
|
|
@@ -5,7 +5,7 @@ from nicegui import run, ui
|
|
|
5
5
|
import bindtools.binding as bd
|
|
6
6
|
|
|
7
7
|
from ..classes import Simulation
|
|
8
|
-
from ..utils import safe_filename
|
|
8
|
+
from ..utils import safe_filename, custom_download
|
|
9
9
|
from .base import BaseComponent
|
|
10
10
|
from .graph import Graph
|
|
11
11
|
|
|
@@ -50,7 +50,7 @@ class SimulationPanel(BaseComponent):
|
|
|
50
50
|
self.save_sim_details_button.set_enabled(False)
|
|
51
51
|
self.download_sim_button = ui.button(
|
|
52
52
|
"Download Simulation Data",
|
|
53
|
-
on_click=
|
|
53
|
+
on_click=self.download_sim_data,
|
|
54
54
|
).classes("ml-4")
|
|
55
55
|
self.export_sim_notebook_button = ui.button(
|
|
56
56
|
"Export to Notebook",
|
|
@@ -325,7 +325,7 @@ class SimulationPanel(BaseComponent):
|
|
|
325
325
|
"""Delete a simulation and update the UI."""
|
|
326
326
|
self.sm.delete_simulation(sim)
|
|
327
327
|
|
|
328
|
-
def download_sim_data(self):
|
|
328
|
+
async def download_sim_data(self):
|
|
329
329
|
"""Download the current simulation data as a CSV file."""
|
|
330
330
|
active_sim = self.sm.active_sim_or_none
|
|
331
331
|
if active_sim is None:
|
|
@@ -335,10 +335,10 @@ class SimulationPanel(BaseComponent):
|
|
|
335
335
|
sim_data = active_sim.results
|
|
336
336
|
csv = sim_data.to_csv(index=False, encoding="utf-8", float_format="{:.5e}".format)
|
|
337
337
|
filename = f"simulation_{safe_filename(active_sim.name, fallback='simulation')}_data.csv"
|
|
338
|
-
|
|
338
|
+
await custom_download(csv, filename=filename)
|
|
339
339
|
ui.notify(f"Simulation data downloaded as {filename}.", type="info")
|
|
340
340
|
|
|
341
|
-
def download_sim_notebook(self) -> None:
|
|
341
|
+
async def download_sim_notebook(self) -> None:
|
|
342
342
|
"""Export the active simulation as a Jupyter notebook (.ipynb) and download it."""
|
|
343
343
|
active_sim = self.sm.active_sim_or_none
|
|
344
344
|
if active_sim is None:
|
|
@@ -354,5 +354,5 @@ class SimulationPanel(BaseComponent):
|
|
|
354
354
|
stem = safe_filename(active_sim.name, fallback="simulation")
|
|
355
355
|
filename = f"{stem}.ipynb"
|
|
356
356
|
content = json.dumps(notebook, indent=1)
|
|
357
|
-
|
|
357
|
+
await custom_download(content, filename=filename)
|
|
358
358
|
ui.notify(f"Notebook exported as {filename}.", type="positive")
|
|
@@ -27,7 +27,7 @@ from ..classes import (
|
|
|
27
27
|
MCMCSim,
|
|
28
28
|
UIBindings,
|
|
29
29
|
)
|
|
30
|
-
from ..utils import eq_mat_from_equation_str_infer_components
|
|
30
|
+
from ..utils import eq_mat_from_equation_str_infer_components, custom_download
|
|
31
31
|
from lmfit import Parameter as LMFitParameter
|
|
32
32
|
import logging
|
|
33
33
|
|
|
@@ -1048,7 +1048,7 @@ class StateManager:
|
|
|
1048
1048
|
# Ensure data is flushed and gzip stream is closed before reading
|
|
1049
1049
|
buffer.seek(0)
|
|
1050
1050
|
filename = f"bindtools_project_{timestamp}.json.gz"
|
|
1051
|
-
|
|
1051
|
+
await custom_download(buffer.read(), filename=filename)
|
|
1052
1052
|
ui.notify(f"Project saved as {filename}", type="info")
|
|
1053
1053
|
|
|
1054
1054
|
def to_json(self):
|
|
@@ -1777,6 +1777,16 @@ bd.makeFitResidPlot(fit,plotMask=(0,1),ylabel='Chemical shift (ppm)')"""
|
|
|
1777
1777
|
model.analytical_linear_obs_columns = lin_cols
|
|
1778
1778
|
model.analytical_linear_obs_param_map = linear_obs_param_map
|
|
1779
1779
|
|
|
1780
|
+
# Always infer topology to allow analytical concentrations in slow exchange
|
|
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
|
+
|
|
1780
1790
|
model.prepModel()
|
|
1781
1791
|
|
|
1782
1792
|
for k in self.active_model.binding_constants:
|
|
@@ -326,3 +326,55 @@ def _infer_simple_fast_exchange_topology(eq_mat: np.ndarray, n_comp: int) -> tup
|
|
|
326
326
|
if len(bound_indices) == 2 and sigs == {(1, 1), (2, 1)}:
|
|
327
327
|
return "2:1", [sig_to_idx[(1, 1)], sig_to_idx[(2, 1)]]
|
|
328
328
|
return None
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
async def custom_download(content: bytes | str, filename: str) -> None:
|
|
332
|
+
"""Download content. If in native mode, open a save dialog; otherwise, use ui.download.content."""
|
|
333
|
+
from nicegui import ui, app
|
|
334
|
+
|
|
335
|
+
# Check if the content is a file-like object and read it
|
|
336
|
+
if hasattr(content, "read"):
|
|
337
|
+
content = content.read()
|
|
338
|
+
elif hasattr(content, "getvalue"):
|
|
339
|
+
content = content.getvalue()
|
|
340
|
+
|
|
341
|
+
# Check if we are running in native mode
|
|
342
|
+
is_native = False
|
|
343
|
+
try:
|
|
344
|
+
if getattr(app.native, "main_window", None) is not None:
|
|
345
|
+
is_native = True
|
|
346
|
+
except Exception:
|
|
347
|
+
pass
|
|
348
|
+
|
|
349
|
+
if is_native:
|
|
350
|
+
try:
|
|
351
|
+
import webview
|
|
352
|
+
|
|
353
|
+
save_dialog_type = getattr(webview, "SAVE_DIALOG", 30)
|
|
354
|
+
if hasattr(webview, "FileDialog") and hasattr(webview.FileDialog, "SAVE"):
|
|
355
|
+
save_dialog_type = webview.FileDialog.SAVE
|
|
356
|
+
|
|
357
|
+
# app.native.main_window is an async wrapper around pywebview window
|
|
358
|
+
selected = await app.native.main_window.create_file_dialog(
|
|
359
|
+
dialog_type=save_dialog_type, save_filename=filename
|
|
360
|
+
)
|
|
361
|
+
if selected:
|
|
362
|
+
filepath = selected[0] if isinstance(selected, (list, tuple)) else selected
|
|
363
|
+
if isinstance(content, str):
|
|
364
|
+
with open(filepath, "w", encoding="utf-8") as f:
|
|
365
|
+
f.write(content)
|
|
366
|
+
else:
|
|
367
|
+
with open(filepath, "wb") as f:
|
|
368
|
+
f.write(content)
|
|
369
|
+
ui.notify(f"File saved to {filepath}", type="positive")
|
|
370
|
+
else:
|
|
371
|
+
ui.notify("Save cancelled", type="warning")
|
|
372
|
+
except Exception as e:
|
|
373
|
+
ui.notify(f"Failed to save file: {str(e)}", type="negative")
|
|
374
|
+
else:
|
|
375
|
+
# Standard browser download
|
|
376
|
+
if isinstance(content, str):
|
|
377
|
+
content_bytes = content.encode("utf-8")
|
|
378
|
+
else:
|
|
379
|
+
content_bytes = content
|
|
380
|
+
ui.download.content(content_bytes, filename=filename)
|
|
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
|