bindmc 0.1.2__tar.gz → 0.1.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {bindmc-0.1.2 → bindmc-0.1.3}/PKG-INFO +5 -5
- {bindmc-0.1.2 → bindmc-0.1.3}/README.md +3 -3
- {bindmc-0.1.2 → bindmc-0.1.3}/pyproject.toml +2 -2
- bindmc-0.1.3/src/bindmc/__main__.py +4 -0
- {bindmc-0.1.2 → bindmc-0.1.3}/src/bindmc/main.py +44 -3
- {bindmc-0.1.2 → bindmc-0.1.3}/src/bindmc/webgui/components/bayes.py +14 -13
- {bindmc-0.1.2 → bindmc-0.1.3}/src/bindmc/webgui/components/data_gen.py +5 -1
- {bindmc-0.1.2 → bindmc-0.1.3}/src/bindmc/webgui/components/fitting.py +5 -5
- {bindmc-0.1.2 → bindmc-0.1.3}/src/bindmc/webgui/components/graph.py +38 -6
- {bindmc-0.1.2 → bindmc-0.1.3}/src/bindmc/webgui/components/simulation.py +6 -6
- {bindmc-0.1.2 → bindmc-0.1.3}/src/bindmc/webgui/state/statemanager.py +2 -2
- {bindmc-0.1.2 → bindmc-0.1.3}/src/bindmc/webgui/utils.py +52 -0
- {bindmc-0.1.2 → bindmc-0.1.3}/src/bindmc/__init__.py +0 -0
- {bindmc-0.1.2 → bindmc-0.1.3}/src/bindmc/webgui/Class model.md +0 -0
- {bindmc-0.1.2 → bindmc-0.1.3}/src/bindmc/webgui/TODO.md +0 -0
- {bindmc-0.1.2 → bindmc-0.1.3}/src/bindmc/webgui/TODO_old.md +0 -0
- {bindmc-0.1.2 → bindmc-0.1.3}/src/bindmc/webgui/__init__.py +0 -0
- {bindmc-0.1.2 → bindmc-0.1.3}/src/bindmc/webgui/app.py +0 -0
- {bindmc-0.1.2 → bindmc-0.1.3}/src/bindmc/webgui/classes/BindingConstant.py +0 -0
- {bindmc-0.1.2 → bindmc-0.1.3}/src/bindmc/webgui/classes/ChemicalShiftParam.py +0 -0
- {bindmc-0.1.2 → bindmc-0.1.3}/src/bindmc/webgui/classes/Component.py +0 -0
- {bindmc-0.1.2 → bindmc-0.1.3}/src/bindmc/webgui/classes/ExptData.py +0 -0
- {bindmc-0.1.2 → bindmc-0.1.3}/src/bindmc/webgui/classes/ExptDataType.py +0 -0
- {bindmc-0.1.2 → bindmc-0.1.3}/src/bindmc/webgui/classes/FitResult.py +0 -0
- {bindmc-0.1.2 → bindmc-0.1.3}/src/bindmc/webgui/classes/MCMCSim.py +0 -0
- {bindmc-0.1.2 → bindmc-0.1.3}/src/bindmc/webgui/classes/Model.py +0 -0
- {bindmc-0.1.2 → bindmc-0.1.3}/src/bindmc/webgui/classes/RawData.py +0 -0
- {bindmc-0.1.2 → bindmc-0.1.3}/src/bindmc/webgui/classes/Simulation.py +0 -0
- {bindmc-0.1.2 → bindmc-0.1.3}/src/bindmc/webgui/classes/UIBindings.py +0 -0
- {bindmc-0.1.2 → bindmc-0.1.3}/src/bindmc/webgui/classes/__init__.py +0 -0
- {bindmc-0.1.2 → bindmc-0.1.3}/src/bindmc/webgui/components/__init__.py +0 -0
- {bindmc-0.1.2 → bindmc-0.1.3}/src/bindmc/webgui/components/base.py +0 -0
- {bindmc-0.1.2 → bindmc-0.1.3}/src/bindmc/webgui/components/bayes_priors.py +0 -0
- {bindmc-0.1.2 → bindmc-0.1.3}/src/bindmc/webgui/components/binding_model.py +0 -0
- {bindmc-0.1.2 → bindmc-0.1.3}/src/bindmc/webgui/components/body.py +0 -0
- {bindmc-0.1.2 → bindmc-0.1.3}/src/bindmc/webgui/components/data_import.py +0 -0
- {bindmc-0.1.2 → bindmc-0.1.3}/src/bindmc/webgui/components/data_model.py +0 -0
- {bindmc-0.1.2 → bindmc-0.1.3}/src/bindmc/webgui/components/header.py +0 -0
- {bindmc-0.1.2 → bindmc-0.1.3}/src/bindmc/webgui/default_models.json +0 -0
- {bindmc-0.1.2 → bindmc-0.1.3}/src/bindmc/webgui/export/__init__.py +0 -0
- {bindmc-0.1.2 → bindmc-0.1.3}/src/bindmc/webgui/export/notebook_exporter.py +0 -0
- {bindmc-0.1.2 → bindmc-0.1.3}/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.3
|
|
4
4
|
Keywords: chemistry,analytical chemistry,binding constants,supramolecular
|
|
5
5
|
Author: Martin Peeks
|
|
6
6
|
Author-email: Martin Peeks <martinp23@googlemail.com>, m.peeks@unsw.edu.au
|
|
@@ -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.1.2
|
|
16
16
|
Requires-Dist: corner==2.2.3
|
|
17
17
|
Requires-Dist: emcee==3.1.6
|
|
18
18
|
Requires-Dist: h5py==3.13.0
|
|
@@ -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.3"
|
|
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.1.2",
|
|
32
32
|
"corner==2.2.3",
|
|
33
33
|
"emcee==3.1.6",
|
|
34
34
|
"h5py==3.13.0",
|
|
@@ -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__"))
|
|
87
|
+
|
|
88
|
+
DEV = not (is_frozen or is_module_run)
|
|
56
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
|
|
|
110
|
+
|
|
72
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("""
|
|
@@ -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
|
|
|
@@ -647,7 +647,7 @@ class FittingPanel(BaseComponent):
|
|
|
647
647
|
self.sm.active_fit.name = self.fit_name_input.value
|
|
648
648
|
self.sm.active_fit.description = self.fit_comment_input.value
|
|
649
649
|
|
|
650
|
-
def download_fit_data_csv(self) -> None:
|
|
650
|
+
async def download_fit_data_csv(self) -> None:
|
|
651
651
|
if self.sm.active_fit_id is None:
|
|
652
652
|
ui.notify("No active fit to download.", type="negative")
|
|
653
653
|
return
|
|
@@ -685,10 +685,10 @@ class FittingPanel(BaseComponent):
|
|
|
685
685
|
export_df = pd.concat(export_frames, axis=1)
|
|
686
686
|
filename = f"fit_{safe_filename(fit.name, fallback='fit')}_data.csv"
|
|
687
687
|
csv = export_df.to_csv(index=False, encoding="utf-8", float_format="{:.5e}".format)
|
|
688
|
-
|
|
688
|
+
await custom_download(csv, filename=filename)
|
|
689
689
|
ui.notify(f"Fit data downloaded as {filename}.", type="info")
|
|
690
690
|
|
|
691
|
-
def download_fit_notebook(self) -> None:
|
|
691
|
+
async def download_fit_notebook(self) -> None:
|
|
692
692
|
"""Export the active fit as a zip containing a Jupyter notebook and a data CSV."""
|
|
693
693
|
if self.sm.active_fit_id is None:
|
|
694
694
|
ui.notify("No active fit to export.", type="negative")
|
|
@@ -712,7 +712,7 @@ class FittingPanel(BaseComponent):
|
|
|
712
712
|
buf.seek(0)
|
|
713
713
|
|
|
714
714
|
zip_filename = f"{stem}_notebook.zip"
|
|
715
|
-
|
|
715
|
+
await custom_download(buf.read(), filename=zip_filename)
|
|
716
716
|
ui.notify(f"Notebook exported as {zip_filename}.", type="positive")
|
|
717
717
|
|
|
718
718
|
|
|
@@ -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):
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|