bindmc 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bindmc/main.py +67 -0
- bindmc/webgui/__init__.py +0 -0
- bindmc/webgui/app.py +54 -0
- bindmc/webgui/classes/BindingConstant.py +23 -0
- bindmc/webgui/classes/ChemicalShiftParam.py +40 -0
- bindmc/webgui/classes/Component.py +111 -0
- bindmc/webgui/classes/ExptData.py +485 -0
- bindmc/webgui/classes/ExptDataType.py +92 -0
- bindmc/webgui/classes/FitResult.py +173 -0
- bindmc/webgui/classes/MCMCSim.py +232 -0
- bindmc/webgui/classes/Model.py +86 -0
- bindmc/webgui/classes/RawData.py +36 -0
- bindmc/webgui/classes/Simulation.py +104 -0
- bindmc/webgui/classes/UIBindings.py +19 -0
- bindmc/webgui/classes/__init__.py +28 -0
- bindmc/webgui/components/__init__.py +29 -0
- bindmc/webgui/components/base.py +24 -0
- bindmc/webgui/components/bayes.py +689 -0
- bindmc/webgui/components/bayes_priors.py +351 -0
- bindmc/webgui/components/binding_model.py +330 -0
- bindmc/webgui/components/body.py +276 -0
- bindmc/webgui/components/data_gen.py +419 -0
- bindmc/webgui/components/data_import.py +450 -0
- bindmc/webgui/components/data_model.py +609 -0
- bindmc/webgui/components/fitting.py +886 -0
- bindmc/webgui/components/graph.py +649 -0
- bindmc/webgui/components/header.py +124 -0
- bindmc/webgui/components/simulation.py +385 -0
- bindmc/webgui/export/__init__.py +0 -0
- bindmc/webgui/export/notebook_exporter.py +727 -0
- bindmc/webgui/state/__init__.py +1 -0
- bindmc/webgui/state/statemanager.py +2043 -0
- bindmc/webgui/utils.py +322 -0
- bindmc-0.1.0.dist-info/METADATA +22 -0
- bindmc-0.1.0.dist-info/RECORD +37 -0
- bindmc-0.1.0.dist-info/WHEEL +5 -0
- bindmc-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import uuid as _uuid
|
|
2
|
+
from .base import BaseComponent
|
|
3
|
+
from nicegui import ui
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class BindToolsHeader(BaseComponent):
|
|
7
|
+
|
|
8
|
+
# (label, dialog_title, collection_attr, active_id_attr, name_attr, reason)
|
|
9
|
+
_ROW_CONFIGS = [
|
|
10
|
+
("Model:", "Select Model", "models", "active_model_id", "name", "header_model_select"),
|
|
11
|
+
("Raw data:", "Select Raw Data", "raw_datas", "active_raw_data_id", "filename", "header_raw_data_select"),
|
|
12
|
+
("Data model:", "Select Data Model", "expt_datas", "active_expt_data_id", "name", "header_data_model_select"),
|
|
13
|
+
("Fit:", "Select Fit", "fits", "active_fit_id", "name", "header_fit_select"),
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
def setup_nicegui(self):
|
|
17
|
+
ui.add_css("""
|
|
18
|
+
.header-menu-btn { position: relative !important; overflow: visible !important; }
|
|
19
|
+
.header-menu-btn.disabled::after {
|
|
20
|
+
content: '';
|
|
21
|
+
position: absolute;
|
|
22
|
+
top: 50%; left: 10%; width: 80%; height: 2px;
|
|
23
|
+
background: currentColor;
|
|
24
|
+
transform: translateY(-50%) rotate(-45deg);
|
|
25
|
+
pointer-events: none;
|
|
26
|
+
}
|
|
27
|
+
""")
|
|
28
|
+
# Dialogs must be created outside the header element (page-level overlays)
|
|
29
|
+
self._rows = []
|
|
30
|
+
for cfg in self._ROW_CONFIGS:
|
|
31
|
+
dialog, radio = self._build_dialog(cfg)
|
|
32
|
+
self._rows.append({"cfg": cfg, "dialog": dialog, "radio": radio})
|
|
33
|
+
|
|
34
|
+
with ui.header().classes(
|
|
35
|
+
"w-full flex justify-between items-center px-4 py-1 bg-black-600 text-white"
|
|
36
|
+
):
|
|
37
|
+
ui.label("BindTools GUI").classes("text-xl font-bold")
|
|
38
|
+
with ui.column().classes("gap-0"):
|
|
39
|
+
for row in self._rows:
|
|
40
|
+
with ui.row().classes("items-center gap-1 p-0 leading-none"):
|
|
41
|
+
row["menu_btn"] = (
|
|
42
|
+
ui.button(icon="menu", on_click=row["dialog"].open)
|
|
43
|
+
.props("flat round dense size=xs")
|
|
44
|
+
.classes("text-white opacity-60 header-menu-btn")
|
|
45
|
+
)
|
|
46
|
+
ui.label(row["cfg"][0]).classes("text-sm opacity-70")
|
|
47
|
+
row["value_label"] = ui.label("").classes("text-sm")
|
|
48
|
+
self._apply_visibility_states()
|
|
49
|
+
|
|
50
|
+
with ui.row():
|
|
51
|
+
ui.button("New Project", on_click=self.sm.new_project).props(
|
|
52
|
+
"unelevated color=primary"
|
|
53
|
+
).classes("q-mx-xs")
|
|
54
|
+
ui.button("Open", on_click=self.sm.open_project).props(
|
|
55
|
+
"unelevated color=secondary"
|
|
56
|
+
).classes("q-mx-xs")
|
|
57
|
+
ui.button("Save", on_click=self.sm.save_project).props(
|
|
58
|
+
"unelevated color=accent"
|
|
59
|
+
).classes("q-mx-xs")
|
|
60
|
+
|
|
61
|
+
def _active_id_str(self, uid) -> str | None:
|
|
62
|
+
return str(uid) if uid is not None else None
|
|
63
|
+
|
|
64
|
+
def _get_options(self, cfg) -> dict:
|
|
65
|
+
_, _, coll_attr, _, name_attr, _ = cfg
|
|
66
|
+
return {str(o.id): getattr(o, name_attr) for o in getattr(self.sm, coll_attr).values()}
|
|
67
|
+
|
|
68
|
+
def _get_active_str(self, cfg) -> str | None:
|
|
69
|
+
return self._active_id_str(getattr(self.sm, cfg[3]))
|
|
70
|
+
|
|
71
|
+
def _apply_visibility_states(self):
|
|
72
|
+
for row in self._rows:
|
|
73
|
+
_, _, coll_attr, active_attr, name_attr, _ = row["cfg"]
|
|
74
|
+
coll = getattr(self.sm, coll_attr)
|
|
75
|
+
obj = coll.get(getattr(self.sm, active_attr))
|
|
76
|
+
row["value_label"].set_text(getattr(obj, name_attr) if obj is not None else "—")
|
|
77
|
+
row["menu_btn"].set_enabled(len(coll) > 1)
|
|
78
|
+
|
|
79
|
+
def _build_dialog(self, cfg):
|
|
80
|
+
_, title, _, _, _, reason = cfg
|
|
81
|
+
with ui.dialog() as dialog, ui.card().classes("min-w-64"):
|
|
82
|
+
ui.label(title).classes("text-base font-bold q-mb-sm")
|
|
83
|
+
radio = ui.radio(options=self._get_options(cfg), value=self._get_active_str(cfg))
|
|
84
|
+
with ui.row().classes("justify-end gap-2 q-mt-sm"):
|
|
85
|
+
ui.button("Cancel", on_click=lambda: self._cancel_dialog(dialog, radio, cfg)).props("flat")
|
|
86
|
+
ui.button("OK", on_click=lambda: self._ok_dialog(dialog, radio, cfg)).props("unelevated color=primary")
|
|
87
|
+
return dialog, radio
|
|
88
|
+
|
|
89
|
+
def _ok_dialog(self, dialog, radio, cfg) -> None:
|
|
90
|
+
_, _, _, active_attr, _, reason = cfg
|
|
91
|
+
val = radio.value
|
|
92
|
+
dialog.close()
|
|
93
|
+
if val is not None:
|
|
94
|
+
setattr(self.sm, active_attr, _uuid.UUID(val))
|
|
95
|
+
self.sm.reconcile_active_context(reason)
|
|
96
|
+
|
|
97
|
+
def _cancel_dialog(self, dialog, radio, cfg) -> None:
|
|
98
|
+
radio.value = self._get_active_str(cfg)
|
|
99
|
+
dialog.close()
|
|
100
|
+
|
|
101
|
+
def setup_bindings(self):
|
|
102
|
+
super().setup_bindings()
|
|
103
|
+
for event in ("model_changed", "expt_data_changed", "fit_changed",
|
|
104
|
+
"fit_completed", "fit_deleted", "sim_changed",
|
|
105
|
+
"data_imported", "active_context_changed"):
|
|
106
|
+
self.sm.add_listener(event, self.refresh_ui_bindings)
|
|
107
|
+
|
|
108
|
+
def refresh_ui_bindings(self, changes=None, *args):
|
|
109
|
+
for row in self._rows:
|
|
110
|
+
row["radio"].options = self._get_options(row["cfg"])
|
|
111
|
+
row["radio"].value = self._get_active_str(row["cfg"])
|
|
112
|
+
self._apply_visibility_states()
|
|
113
|
+
|
|
114
|
+
if isinstance(changes, dict):
|
|
115
|
+
_warn_attrs = {
|
|
116
|
+
"_active_sim_id": "simulation",
|
|
117
|
+
"_active_fit_id": "fit",
|
|
118
|
+
"_active_expt_data_id": "data model",
|
|
119
|
+
"_active_raw_data_id": "raw data",
|
|
120
|
+
}
|
|
121
|
+
for attr, label in _warn_attrs.items():
|
|
122
|
+
entry = changes.get(attr)
|
|
123
|
+
if entry and entry[0] is not None and entry[1] is None:
|
|
124
|
+
ui.notify(f"No {label} available for the selected context.", type="warning")
|
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import json
|
|
3
|
+
import zipfile
|
|
4
|
+
import numpy as np
|
|
5
|
+
import pandas as pd
|
|
6
|
+
from nicegui import run, ui
|
|
7
|
+
import asyncio
|
|
8
|
+
import bindtools.binding as bd
|
|
9
|
+
|
|
10
|
+
from ..classes import Simulation
|
|
11
|
+
from ..utils import safe_filename
|
|
12
|
+
from .base import BaseComponent
|
|
13
|
+
from .graph import Graph
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SimulationPanel(BaseComponent):
|
|
17
|
+
def setup_nicegui(self):
|
|
18
|
+
|
|
19
|
+
self.container = ui.column().classes("w-full")
|
|
20
|
+
with self.container:
|
|
21
|
+
with ui.row().classes("w-full mb-4"):
|
|
22
|
+
ui.button("Run Simulation", on_click=self.run_simulation).classes(
|
|
23
|
+
"mb-4"
|
|
24
|
+
)
|
|
25
|
+
self.clear_graph_button = ui.button("Clear Graph").classes("mb-4")
|
|
26
|
+
# Dropdown for loading and deleting simulations
|
|
27
|
+
self.sim_dropdown = ui.dropdown_button("Load/Delete Simulation", auto_close=True).classes("mb-4")
|
|
28
|
+
self.generate_sims_dropdown()
|
|
29
|
+
|
|
30
|
+
# Add name and comment inputs with save button
|
|
31
|
+
with ui.card().classes("w-full mb-4"):
|
|
32
|
+
ui.label("Simulation Details")
|
|
33
|
+
with ui.row().classes("w-full"):
|
|
34
|
+
self.sim_name_input = ui.input(
|
|
35
|
+
"Simulation Name", placeholder="Enter simulation name"
|
|
36
|
+
).classes("flex-1")
|
|
37
|
+
self.sim_comment_input = ui.input(
|
|
38
|
+
"Comment", placeholder="Enter comment"
|
|
39
|
+
).classes("flex-1")
|
|
40
|
+
ui.button(
|
|
41
|
+
"Auto-generate title",
|
|
42
|
+
on_click=lambda: self.sim_name_input.set_value(
|
|
43
|
+
self.sm.active_model.name
|
|
44
|
+
+ " "
|
|
45
|
+
+ ", ".join(
|
|
46
|
+
[
|
|
47
|
+
f"{k.species}={k.logK}"
|
|
48
|
+
for k in self.sm.active_model.binding_constants
|
|
49
|
+
if not k.isComp
|
|
50
|
+
]
|
|
51
|
+
)
|
|
52
|
+
),
|
|
53
|
+
).classes("ml-4")
|
|
54
|
+
self.save_sim_details_button = ui.button(
|
|
55
|
+
"Save changes to name/comment", on_click=self.save_sim_details
|
|
56
|
+
).classes("ml-4")
|
|
57
|
+
self.save_sim_details_button.set_enabled(False)
|
|
58
|
+
self.download_sim_button = ui.button(
|
|
59
|
+
"Download Simulation Data",
|
|
60
|
+
on_click=lambda: self.download_sim_data(),
|
|
61
|
+
).classes("ml-4")
|
|
62
|
+
self.export_sim_notebook_button = ui.button(
|
|
63
|
+
"Export to Notebook",
|
|
64
|
+
on_click=self.download_sim_notebook,
|
|
65
|
+
).classes("ml-4")
|
|
66
|
+
|
|
67
|
+
self.graphEl = ui.element().classes("w-full lg:w-[min(60vw,80vh)] relative")
|
|
68
|
+
with self.graphEl:
|
|
69
|
+
self.graph = Graph(self.sm, mode="sim")
|
|
70
|
+
self.spinner = ui.spinner(size="xl").classes("absolute-center")
|
|
71
|
+
|
|
72
|
+
self.clear_graph_button.on_click(self.graph.clear_graph)
|
|
73
|
+
self.spinner.visible = False
|
|
74
|
+
|
|
75
|
+
def setup_bindings(self):
|
|
76
|
+
active_sim = self.sm.active_sim_or_none
|
|
77
|
+
if active_sim is not None:
|
|
78
|
+
self.sim_name_input.set_value(active_sim.name)
|
|
79
|
+
self.sim_comment_input.set_value(active_sim.comment)
|
|
80
|
+
self.save_sim_details_button.set_enabled(True)
|
|
81
|
+
|
|
82
|
+
self.sm.add_listener("simulation_deleted", self.update_after_sim_deleted)
|
|
83
|
+
self.sm.add_listener("sim_changed", self._refresh_for_active_sim)
|
|
84
|
+
|
|
85
|
+
def _refresh_for_active_sim(self, e=None):
|
|
86
|
+
"""Refresh the simulation panel to reflect the current active simulation."""
|
|
87
|
+
self.generate_sims_dropdown()
|
|
88
|
+
active_sim = self.sm.active_sim_or_none
|
|
89
|
+
if active_sim is None:
|
|
90
|
+
self.sim_name_input.set_value("")
|
|
91
|
+
self.sim_comment_input.set_value("")
|
|
92
|
+
self.save_sim_details_button.set_enabled(False)
|
|
93
|
+
self.graph.clear_graph()
|
|
94
|
+
ui.notify("No simulations for the current context.", type="warning")
|
|
95
|
+
return
|
|
96
|
+
self.sim_name_input.set_value(active_sim.name)
|
|
97
|
+
self.sim_comment_input.set_value(active_sim.comment)
|
|
98
|
+
self.save_sim_details_button.set_enabled(True)
|
|
99
|
+
if active_sim.results is not None and not active_sim.results.empty:
|
|
100
|
+
self.graph.clear_graph()
|
|
101
|
+
self.graph.add_graph_lines_xy(
|
|
102
|
+
active_sim.comp_concs, active_sim.results, active_sim.name, str(active_sim.id)
|
|
103
|
+
)
|
|
104
|
+
self.graph.update_graph()
|
|
105
|
+
|
|
106
|
+
async def run_simulation(self, e):
|
|
107
|
+
"""Run the simulation based on the current model data."""
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
# Check if the simulation name looks auto-generated but doesn't match the current auto-generated value
|
|
111
|
+
auto_name = (
|
|
112
|
+
self.sm.active_model.name
|
|
113
|
+
+ " "
|
|
114
|
+
+ ", ".join(
|
|
115
|
+
[
|
|
116
|
+
f"{k.species}={k.logK}"
|
|
117
|
+
for k in self.sm.active_model.binding_constants
|
|
118
|
+
if not k.isComp
|
|
119
|
+
]
|
|
120
|
+
)
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
# if there is no simulation name, prmopt the user to cancel or use the auto-generated name
|
|
125
|
+
if not self.sim_name_input.value:
|
|
126
|
+
with ui.dialog() as dialog, ui.card():
|
|
127
|
+
ui.label(
|
|
128
|
+
f"Simulation name is empty. Do you want to use the auto-generated name: \n{auto_name}?"
|
|
129
|
+
).style('white-space: pre-wrap;')
|
|
130
|
+
with ui.row():
|
|
131
|
+
ui.button("Use auto-generated name", on_click=lambda: dialog.submit(auto_name))
|
|
132
|
+
ui.button("Cancel", on_click=lambda: dialog.submit(None))
|
|
133
|
+
|
|
134
|
+
async def show_dialog():
|
|
135
|
+
result = await dialog
|
|
136
|
+
return result
|
|
137
|
+
|
|
138
|
+
res = await show_dialog()
|
|
139
|
+
if res is None:
|
|
140
|
+
ui.notify(
|
|
141
|
+
"Simulation not run. Please enter a simulation name.",
|
|
142
|
+
type="info",
|
|
143
|
+
)
|
|
144
|
+
return
|
|
145
|
+
else:
|
|
146
|
+
self.sim_name_input.set_value(res)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
if (
|
|
151
|
+
self.sim_name_input.value.startswith(self.sm.active_model.name)
|
|
152
|
+
and self.sim_name_input.value != auto_name
|
|
153
|
+
):
|
|
154
|
+
with ui.dialog() as dialog, ui.card():
|
|
155
|
+
ui.label(
|
|
156
|
+
"Warning: The simulation name appears auto-generated but does not match the current model and constants. Consider updating the name."
|
|
157
|
+
)
|
|
158
|
+
with ui.row():
|
|
159
|
+
ui.button("Continue", on_click=lambda: dialog.submit("continue"))
|
|
160
|
+
ui.button(
|
|
161
|
+
"Generate new name and run",
|
|
162
|
+
on_click=lambda: dialog.submit("update"),
|
|
163
|
+
)
|
|
164
|
+
ui.button("Cancel", on_click=lambda: dialog.submit("cancel"))
|
|
165
|
+
|
|
166
|
+
async def show_dialog():
|
|
167
|
+
result = await dialog
|
|
168
|
+
return result
|
|
169
|
+
|
|
170
|
+
res = await show_dialog()
|
|
171
|
+
if res == "cancel":
|
|
172
|
+
ui.notify(
|
|
173
|
+
"Simulation not run. Please choose a different name or update the current one.",
|
|
174
|
+
type="info",
|
|
175
|
+
)
|
|
176
|
+
return
|
|
177
|
+
elif res == "continue":
|
|
178
|
+
ui.notify("Continuing with the simulation.", type="info")
|
|
179
|
+
elif res == "update":
|
|
180
|
+
# Update the simulation name to the auto-generated value
|
|
181
|
+
self.sim_name_input.set_value(auto_name)
|
|
182
|
+
ui.notify(
|
|
183
|
+
f"Simulation name updated to '{auto_name}'; now running.",
|
|
184
|
+
type="info",
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
if any(sim.name == self.sim_name_input.value for sim in self.sm.simulations.values()):
|
|
189
|
+
with ui.dialog() as dialog, ui.card():
|
|
190
|
+
ui.label(
|
|
191
|
+
f"A simulation named '{self.sim_name_input.value}' already exists. Do you want to overwrite it?"
|
|
192
|
+
)
|
|
193
|
+
with ui.row():
|
|
194
|
+
ui.button("Overwrite", on_click=lambda: dialog.submit(True))
|
|
195
|
+
ui.button("Cancel", on_click=lambda: dialog.submit(False))
|
|
196
|
+
|
|
197
|
+
async def show_dialog():
|
|
198
|
+
result = await dialog
|
|
199
|
+
return result
|
|
200
|
+
|
|
201
|
+
res = await show_dialog()
|
|
202
|
+
if not res:
|
|
203
|
+
ui.notify(
|
|
204
|
+
"Simulation not run. Please choose a different name or delete the old simulation.",
|
|
205
|
+
type="info",
|
|
206
|
+
)
|
|
207
|
+
return
|
|
208
|
+
else:
|
|
209
|
+
# Delete the existing simulation with that name
|
|
210
|
+
existing_sim = next(
|
|
211
|
+
(
|
|
212
|
+
sim
|
|
213
|
+
for sim in self.sm.simulations.values()
|
|
214
|
+
if sim.name == self.sim_name_input.value
|
|
215
|
+
),
|
|
216
|
+
None,
|
|
217
|
+
)
|
|
218
|
+
if existing_sim:
|
|
219
|
+
self.sm.delete_simulation(existing_sim, notify_listeners=False)
|
|
220
|
+
ui.notify(
|
|
221
|
+
f"Overwriting simulation '{self.sim_name_input.value}'.",
|
|
222
|
+
type="info",
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
try:
|
|
226
|
+
eq_mat = []
|
|
227
|
+
if self.sm.eq_mat_str: # TODO this is weird. fix it.
|
|
228
|
+
eq_mat = np.array(self.sm.eq_mat)
|
|
229
|
+
|
|
230
|
+
comp_concs = self.sm.comp_concs.to_numpy()
|
|
231
|
+
m1 = bd.bindingModel(
|
|
232
|
+
eq_mat, self.sm.component_names, self.sm.species, compConcs=comp_concs
|
|
233
|
+
)
|
|
234
|
+
m1.prepModel()
|
|
235
|
+
|
|
236
|
+
# set parameter values
|
|
237
|
+
for s in self.sm.species:
|
|
238
|
+
k = [k for k in self.sm.binding_constants if k.species == s][0]
|
|
239
|
+
m1.params["log" + k.species].set(value=k.logK,max=k.logK+1 if k.logK is not None else None, min=k.logK-1 if k.logK is not None else None)
|
|
240
|
+
|
|
241
|
+
self.spinner.visible = True
|
|
242
|
+
|
|
243
|
+
spec = await run.cpu_bound(m1.calcSpeciation)
|
|
244
|
+
calc_result = pd.DataFrame(
|
|
245
|
+
spec, columns=[x + "_free" for x in self.sm.species]
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
new_sim = Simulation(
|
|
249
|
+
comp_concs=self.sm.comp_concs.copy(),
|
|
250
|
+
model_id=self.sm.active_model.id,
|
|
251
|
+
name=self.sim_name_input.value,
|
|
252
|
+
comment=self.sim_comment_input.value,
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
self.sm.add_sim(new_sim)
|
|
256
|
+
currSim = new_sim
|
|
257
|
+
currSim.find_and_link_model(self.sm.models)
|
|
258
|
+
currSim.comp_concs.columns = [f'{c}_tot' for c in currSim.comp_concs.columns]
|
|
259
|
+
#cols = [f'{c}_tot' for c in self.sm.comp_concs.columns]
|
|
260
|
+
#currSim.comp_concs.columns = [f'{c}_tot' for c in currSim.comp_concs.columns]
|
|
261
|
+
|
|
262
|
+
comp_concs_df = currSim.comp_concs#pd.DataFrame(self.sm.comp_concs.values,columns=cols)
|
|
263
|
+
|
|
264
|
+
# # Fix index mismatch - reset both indexes to ensure they match
|
|
265
|
+
comp_concs_df = comp_concs_df.reset_index(drop=True)
|
|
266
|
+
calc_result = calc_result.reset_index(drop=True)
|
|
267
|
+
|
|
268
|
+
currSim.results = comp_concs_df.merge(calc_result,left_index=True,right_index=True,how='inner')
|
|
269
|
+
|
|
270
|
+
# Enable save button
|
|
271
|
+
self.save_sim_details_button.set_enabled(True)
|
|
272
|
+
|
|
273
|
+
self.spinner.visible = False
|
|
274
|
+
|
|
275
|
+
self.graph.add_graph_lines_xy(comp_concs_df,calc_result, currSim.name, str(currSim.id))
|
|
276
|
+
self.graph.update_graph()
|
|
277
|
+
self.generate_sims_dropdown()
|
|
278
|
+
self.sm.save_to_storage()
|
|
279
|
+
ui.notify(f"Simulation {currSim.name} completed successfully.", type="info")
|
|
280
|
+
|
|
281
|
+
except Exception as e:
|
|
282
|
+
ui.notify("Error creating binding model: " + str(e), type="negative")
|
|
283
|
+
return
|
|
284
|
+
|
|
285
|
+
def generate_sims_dropdown(self):
|
|
286
|
+
self.sim_dropdown.clear()
|
|
287
|
+
with self.sim_dropdown:
|
|
288
|
+
for sim in self.sm.simulations.values():
|
|
289
|
+
with ui.row():
|
|
290
|
+
ui.item(
|
|
291
|
+
sim.name,
|
|
292
|
+
on_click=lambda sim=sim: self.load_simulation(sim),
|
|
293
|
+
)
|
|
294
|
+
ui.button(
|
|
295
|
+
"Delete",
|
|
296
|
+
color="negative",
|
|
297
|
+
icon="delete",
|
|
298
|
+
on_click=lambda sim=sim: self.delete_simulation(sim),
|
|
299
|
+
).classes("ml-2")
|
|
300
|
+
|
|
301
|
+
def save_sim_details(self, e):
|
|
302
|
+
"""Save the simulation name and comment."""
|
|
303
|
+
if self.sm.active_sim:
|
|
304
|
+
self.sm.active_sim.name = self.sim_name_input.value
|
|
305
|
+
self.sm.active_sim.comment = self.sim_comment_input.value
|
|
306
|
+
ui.notify(
|
|
307
|
+
f"Simulation details saved: {self.sm.active_sim.name}", type="info"
|
|
308
|
+
)
|
|
309
|
+
self.sm.save_to_storage(e)
|
|
310
|
+
|
|
311
|
+
def update_after_sim_deleted(self, e=None):
|
|
312
|
+
"""Update the UI after (one or more) simulation is deleted."""
|
|
313
|
+
self.generate_sims_dropdown()
|
|
314
|
+
active_sim = self.sm.active_sim_or_none
|
|
315
|
+
if self.sm.active_sim_id is not None and active_sim is not None:
|
|
316
|
+
self.sim_name_input.set_value(active_sim.name)
|
|
317
|
+
self.sim_comment_input.set_value(active_sim.comment)
|
|
318
|
+
else:
|
|
319
|
+
self.sim_name_input.set_value("")
|
|
320
|
+
self.sim_comment_input.set_value("")
|
|
321
|
+
self.save_sim_details_button.set_enabled(False)
|
|
322
|
+
|
|
323
|
+
sim_ids = [str(sim.id) for sim in self.sm.simulations.values()]
|
|
324
|
+
missing_ids = list(set([
|
|
325
|
+
row["trace_id"][0:36] for row in self.graph.graph_data["data"] if row["trace_id"][0:36] not in sim_ids
|
|
326
|
+
]))
|
|
327
|
+
|
|
328
|
+
for id in missing_ids:
|
|
329
|
+
self.graph.remove_data(id)
|
|
330
|
+
|
|
331
|
+
self.generate_sims_dropdown()
|
|
332
|
+
self.graph.update_x_axis_selects()
|
|
333
|
+
self.graph.update_graph_x()
|
|
334
|
+
self.graph.graph.update()
|
|
335
|
+
|
|
336
|
+
def load_simulation(self, sim):
|
|
337
|
+
"""Load a simulation and update the UI."""
|
|
338
|
+
self.sm.active_sim_id = sim.id
|
|
339
|
+
self.sim_name_input.set_value(sim.name)
|
|
340
|
+
self.sim_comment_input.set_value(sim.comment)
|
|
341
|
+
self.save_sim_details_button.set_enabled(True)
|
|
342
|
+
|
|
343
|
+
# Update the graph with the loaded simulation data
|
|
344
|
+
self.graph.clear_graph()
|
|
345
|
+
self.graph.add_graph_lines_xy(
|
|
346
|
+
sim.comp_concs, sim.results, sim.name, str(sim.id)
|
|
347
|
+
)
|
|
348
|
+
self.graph.update_graph()
|
|
349
|
+
ui.notify(f"Loaded simulation: {sim.name}", type="info")
|
|
350
|
+
|
|
351
|
+
def delete_simulation(self, sim):
|
|
352
|
+
"""Delete a simulation and update the UI."""
|
|
353
|
+
self.sm.delete_simulation(sim)
|
|
354
|
+
|
|
355
|
+
def download_sim_data(self):
|
|
356
|
+
"""Download the current simulation data as a CSV file."""
|
|
357
|
+
active_sim = self.sm.active_sim_or_none
|
|
358
|
+
if active_sim is None:
|
|
359
|
+
ui.notify("No active simulation to download.", type="negative")
|
|
360
|
+
return
|
|
361
|
+
|
|
362
|
+
sim_data = active_sim.results
|
|
363
|
+
csv=sim_data.to_csv(index=False,encoding='utf-8',float_format="{:.5e}".format)
|
|
364
|
+
filename = f"simulation_{safe_filename(active_sim.name, fallback='simulation')}_data.csv"
|
|
365
|
+
ui.download.content(csv, filename=filename)
|
|
366
|
+
ui.notify(f"Simulation data downloaded as {filename}.", type="info")
|
|
367
|
+
|
|
368
|
+
def download_sim_notebook(self) -> None:
|
|
369
|
+
"""Export the active simulation as a Jupyter notebook (.ipynb) and download it."""
|
|
370
|
+
active_sim = self.sm.active_sim_or_none
|
|
371
|
+
if active_sim is None:
|
|
372
|
+
ui.notify("No active simulation to export.", type="negative")
|
|
373
|
+
return
|
|
374
|
+
|
|
375
|
+
try:
|
|
376
|
+
notebook = self.sm.dump_simulation_notebook(active_sim)
|
|
377
|
+
except Exception as exc:
|
|
378
|
+
ui.notify(f"Notebook export failed: {exc}", type="negative")
|
|
379
|
+
return
|
|
380
|
+
|
|
381
|
+
stem = safe_filename(active_sim.name, fallback="simulation")
|
|
382
|
+
filename = f"{stem}.ipynb"
|
|
383
|
+
content = json.dumps(notebook, indent=1)
|
|
384
|
+
ui.download.content(content, filename=filename)
|
|
385
|
+
ui.notify(f"Notebook exported as {filename}.", type="positive")
|
|
File without changes
|