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.
Files changed (37) hide show
  1. bindmc/main.py +67 -0
  2. bindmc/webgui/__init__.py +0 -0
  3. bindmc/webgui/app.py +54 -0
  4. bindmc/webgui/classes/BindingConstant.py +23 -0
  5. bindmc/webgui/classes/ChemicalShiftParam.py +40 -0
  6. bindmc/webgui/classes/Component.py +111 -0
  7. bindmc/webgui/classes/ExptData.py +485 -0
  8. bindmc/webgui/classes/ExptDataType.py +92 -0
  9. bindmc/webgui/classes/FitResult.py +173 -0
  10. bindmc/webgui/classes/MCMCSim.py +232 -0
  11. bindmc/webgui/classes/Model.py +86 -0
  12. bindmc/webgui/classes/RawData.py +36 -0
  13. bindmc/webgui/classes/Simulation.py +104 -0
  14. bindmc/webgui/classes/UIBindings.py +19 -0
  15. bindmc/webgui/classes/__init__.py +28 -0
  16. bindmc/webgui/components/__init__.py +29 -0
  17. bindmc/webgui/components/base.py +24 -0
  18. bindmc/webgui/components/bayes.py +689 -0
  19. bindmc/webgui/components/bayes_priors.py +351 -0
  20. bindmc/webgui/components/binding_model.py +330 -0
  21. bindmc/webgui/components/body.py +276 -0
  22. bindmc/webgui/components/data_gen.py +419 -0
  23. bindmc/webgui/components/data_import.py +450 -0
  24. bindmc/webgui/components/data_model.py +609 -0
  25. bindmc/webgui/components/fitting.py +886 -0
  26. bindmc/webgui/components/graph.py +649 -0
  27. bindmc/webgui/components/header.py +124 -0
  28. bindmc/webgui/components/simulation.py +385 -0
  29. bindmc/webgui/export/__init__.py +0 -0
  30. bindmc/webgui/export/notebook_exporter.py +727 -0
  31. bindmc/webgui/state/__init__.py +1 -0
  32. bindmc/webgui/state/statemanager.py +2043 -0
  33. bindmc/webgui/utils.py +322 -0
  34. bindmc-0.1.0.dist-info/METADATA +22 -0
  35. bindmc-0.1.0.dist-info/RECORD +37 -0
  36. bindmc-0.1.0.dist-info/WHEEL +5 -0
  37. 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