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,450 @@
1
+ import io
2
+ import math
3
+
4
+ import pandas as pd
5
+ from nicegui import ui
6
+ from nicegui.events import UploadEventArguments, ClickEventArguments
7
+
8
+ from .base import BaseComponent
9
+ from .graph import Graph
10
+ from ..classes import ExptData, RawData, ExptDataType
11
+
12
+
13
+ class DataImportPanel(BaseComponent):
14
+ def __init__(self, sm):
15
+ super().__init__(sm)
16
+ self.dep_indep_dropdowns = []
17
+ self.dep_indep_labels = []
18
+ self.ignore_checkboxes = []
19
+
20
+ self.dtype_labels = []
21
+ self.dtype_dropdowns = []
22
+
23
+ def setup_nicegui(self):
24
+ self.container = ui.column().classes("w-full")
25
+
26
+ with self.container:
27
+ ui.label("Data Import panel").classes("text-lg font-bold mb-4")
28
+ # Responsive layout:
29
+ # - narrow screens: stack cards top-to-bottom
30
+ # - wide screens: show cards side-by-side (controls left, table/graph right)
31
+ with ui.row().classes("w-full gap-4 items-start flex-col lg:flex-row"):
32
+ with ui.card().classes("w-full lg:flex-1 min-w-0"):
33
+ active_expt = self.sm.active_expt_data_or_none
34
+ if active_expt is not None:
35
+ self.expt_data_dropdown_button = (
36
+ ui.dropdown_button("Choose dataset", auto_close=True)
37
+ .bind_text(active_expt, "name")
38
+ .classes("mb-5")
39
+ )
40
+ else:
41
+ self.expt_data_dropdown_button = (
42
+ ui.dropdown_button("No active model", auto_close=True)
43
+ .classes("mb-5")
44
+ )
45
+
46
+ if len(self.sm.raw_datas) > 0:
47
+ self.generate_data_dropdown()
48
+ else:
49
+ self.expt_data_dropdown_button.visible = False
50
+
51
+ ui.label("Upload Data File (CSV or Excel)")
52
+ ui.button("Upload File", on_click=self.load_exptdata)
53
+
54
+ self.expt_data_col_block = ui.element()
55
+ with self.expt_data_col_block:
56
+ ui.label("Column Metadata:")
57
+
58
+ self.make_model_button = ui.button(
59
+ "Prepare data model", on_click=self.prepare_data_model
60
+ )
61
+
62
+ with ui.card().classes("w-full lg:flex-1 min-w-0"):
63
+ ui.label("Data")
64
+ self.table_and_graph = ui.element().classes("w-full")
65
+ with self.table_and_graph:
66
+ with (
67
+ ui.tabs()
68
+ .classes("w-full")
69
+ .props('align="justify"')
70
+ .on("update:model-value", self._table_graph_tab_changed)
71
+ ) as tabs:
72
+ self.table_tab = ui.tab("Table")
73
+ self.graph_tab = ui.tab("Graph")
74
+
75
+ with ui.tab_panels(tabs, value=self.table_tab).classes("w-full"):
76
+ with ui.tab_panel(self.table_tab):
77
+ self.expt_data_table_block = ui.element().classes("w-full")
78
+
79
+ with ui.tab_panel(self.graph_tab):
80
+ ui.label("Experimental data preview")
81
+ self.preview_graph = Graph(self.sm, mode="expt_preview")
82
+
83
+
84
+ def _table_graph_tab_changed(self, e) -> None:
85
+ if getattr(e, "args", None) != "Graph":
86
+ return
87
+ if not hasattr(self, "preview_graph"):
88
+ return
89
+ try:
90
+ self.preview_graph.graph.update()
91
+ except Exception:
92
+ pass
93
+ try:
94
+ ui.run_javascript('window.dispatchEvent(new Event("resize"));')
95
+ except Exception:
96
+ pass
97
+
98
+
99
+ def setup_bindings(self):
100
+ super().setup_bindings()
101
+ self.sm.add_listener("data_imported", self._load_data_to_table)
102
+ self.sm.add_listener("data_imported", self.generate_data_dropdown)
103
+ self.sm.add_listener("active_context_changed", self._load_data_to_table)
104
+ self.sm.add_listener("active_context_changed", self.generate_data_dropdown)
105
+ self.sm.add_listener("expt_data_columns_changed", self._load_data_to_table) # Update when column selection changes
106
+
107
+ def prepare_data_model(self, e: ClickEventArguments):
108
+ active_raw = self.sm.active_raw_data_or_none
109
+ active_expt = self.sm.active_expt_data_or_none
110
+ if self.sm.active_raw_data_id is not None and active_raw is not None:
111
+ if active_expt is not None and active_expt.raw_data_id == active_raw.id:
112
+ # Auto-deselect unassigned columns
113
+ for col in active_expt.data.columns:
114
+ col_details = active_expt.col_details.get(col, {})
115
+ is_component = col.startswith('[') and col.endswith(']')
116
+ has_assignment = (col_details.get('depindep') in ['dep', 'indep']) and (col_details.get('dtype') is not None)
117
+
118
+ if not is_component and not has_assignment:
119
+ # Remove from selected columns instead of marking as ignored
120
+ if col in active_expt.selected_columns:
121
+ active_expt.selected_columns.remove(col)
122
+
123
+ self._load_expt_data_col_details() # Refresh UI to show changes
124
+ self._load_data_to_table() # Refresh table and graph to show selected data
125
+ ui.notify("Data model prepared. Unassigned columns have been deselected.", type="positive")
126
+ else:
127
+ rd = active_raw
128
+ new_expt_data = ExptData(name=rd.filename,init_raw_data=rd, init_model=self.sm.active_model)
129
+ self.sm.add_expt_data(new_expt_data)
130
+ else:
131
+ ui.notify("No raw data selected to prepare data model from.", type="negative")
132
+
133
+ async def load_exptdata(self):
134
+ try:
135
+ with ui.dialog() as dialog, ui.card():
136
+ ui.label("Load experimental data file")
137
+ upload_box = ui.upload(label="Choose file", auto_upload=True).props(
138
+ 'accept=".csv, .xlsx, .xls"'
139
+ )
140
+ ui.button("Cancel", on_click=lambda: dialog.submit("cancel"))
141
+
142
+ def on_upload_complete(e: UploadEventArguments):
143
+ dialog.submit(e) # Store result for later
144
+
145
+ upload_box.on_upload(on_upload_complete)
146
+
147
+ result = await dialog
148
+ if isinstance(result, str) and result == "cancel":
149
+ ui.notify("Experimental data loading cancelled", type="info")
150
+ return
151
+ elif isinstance(result, UploadEventArguments):
152
+ data = pd.DataFrame() # Initialize an empty DataFrame
153
+ if result.file.name.endswith(".xlsx") or result.file.name.endswith(".xls"):
154
+ # If the file is an Excel file, read it into a DataFrame
155
+ file_content = await result.file.read()
156
+ data = pd.read_excel(io.BytesIO(file_content))
157
+
158
+ elif result.file.name.endswith(".csv"):
159
+ # If the file is a CSV file, read it into a DataFrame
160
+ file_content = await result.file.text(encoding='utf-8')
161
+ data = pd.read_csv(io.StringIO(file_content))
162
+
163
+ name = result.file.name
164
+ # if result.name is already in the list of expt_datas, append a number to the name
165
+ if any(ed.name == name for ed in self.sm.expt_datas.values()):
166
+ count = 1
167
+ while any(ed.name == f"{name}_({count})" for ed in self.sm.expt_datas.values()):
168
+ count += 1
169
+ name = f"{name}_({count})"
170
+
171
+ rd = RawData(filename=name, data=data)
172
+ self.sm.add_raw_data(rd) # Add raw data to the state manager
173
+ self.sm.add_expt_data(ExptData(name=name,init_raw_data=rd,
174
+ init_model=self.sm.active_model) ) # Load CSV data into a DataFrame
175
+
176
+
177
+ # self.sm.expt_data.col_details = {c: {'depindep': None} for i,c in enumerate(self.sm.expt_data.data.columns)}
178
+ ui.notify("Experimental data loaded successfully", type="info")
179
+ self.sm.notify_listeners("data_imported")
180
+ else:
181
+ ui.notify("No file uploaded or unsupported file type", type="negative")
182
+ return
183
+ except Exception as e:
184
+ print(f"Error loading data: {str(e)}")
185
+ ui.notify("Failed to load data", type="negative")
186
+
187
+ def _load_data_to_table(self, e=None):
188
+ """Load the experimental data into the table."""
189
+ self.expt_data_table_block.clear()
190
+ if self.sm.active_expt_data_id is not None and self.sm.active_expt_data.data is not None and not self.sm.active_expt_data.data.empty:
191
+
192
+ with self.expt_data_table_block:
193
+ if not self.sm.active_expt_data.data.empty:
194
+ ui.label("Experimental Data:")
195
+ with (
196
+ ui.element("div")
197
+ .classes("w-full max-h-[30rem] overflow-y-scroll")
198
+ .style("scrollbar-gutter: stable;")
199
+ ):
200
+ # Show selected data in the table if column selection is active
201
+ data_to_display = self.sm.active_expt_data.selected_data
202
+ if data_to_display.empty:
203
+ data_to_display = self.sm.active_expt_data.data # Fallback to all data if no selection
204
+
205
+ self.expt_dataTable = (
206
+ ui.table.from_pandas(data_to_display)
207
+ .classes("w-full")
208
+ .props('dense hide-bottom :pagination="{rowsPerPage: 0}"')
209
+ .mark("expt-data-table")
210
+ )
211
+ else:
212
+ ui.label("No experimental data loaded.")
213
+ self._load_expt_data_col_details()
214
+ self.preview_graph.clear_graph()
215
+ # Show only selected data in the graph preview
216
+ selected_data_to_show = self.sm.active_expt_data.selected_data
217
+ if not selected_data_to_show.empty:
218
+ self.preview_graph.add_graph_lines(
219
+ selected_data_to_show,
220
+ run_name="Expt (selected)",
221
+ run_id=str(self.sm.active_expt_data_id),
222
+ scatter="markers",
223
+ )
224
+ self.preview_graph.update_graph()
225
+ else: # i.e. there is no data, we have deleted the last data
226
+ self.expt_data_col_block.clear()
227
+ self.preview_graph.clear_graph()
228
+ self.preview_graph.update_graph()
229
+
230
+ def _load_expt_data_col_details(self):
231
+ self.expt_data_col_block.clear()
232
+ # Clear UI element lists to prevent stale references
233
+ self.dep_indep_dropdowns.clear()
234
+ self.dep_indep_labels.clear()
235
+ self.ignore_checkboxes.clear()
236
+ self.dtype_labels.clear()
237
+ self.dtype_dropdowns.clear()
238
+
239
+ with self.expt_data_col_block:
240
+ if not self.sm.active_expt_data.data.empty:
241
+ ui.label("Experimental Data Columns:")
242
+ # populate column details dict
243
+ # {col: {'depindep': None, ...}}
244
+
245
+ # Ensure selected_columns is initialized once before creating UI
246
+ if not self.sm.active_expt_data.selected_columns:
247
+ self.sm.active_expt_data.selected_columns = self.sm.active_expt_data.data.columns.tolist()
248
+
249
+ for col in self.sm.active_expt_data.data.columns: # Show all original columns
250
+
251
+ with ui.card() as card:
252
+ with ui.row().classes('items-center gap-2'):
253
+ # Column name label
254
+ label = ui.label(col).classes("text-sm font-semibold")
255
+ self.dep_indep_labels.append(label)
256
+
257
+ # "Include this column" checkbox
258
+ is_selected = col in self.sm.active_expt_data.selected_columns
259
+ include_cb = ui.checkbox("Include this column", value=is_selected)
260
+ self.ignore_checkboxes.append(include_cb) # Reuse existing list for simplicity
261
+
262
+ # Dep/Indep radio buttons
263
+ with ui.row().classes('items-center gap-2'):
264
+ dep_indep_radio = ui.radio({
265
+ 'indep': 'Independent variable',
266
+ 'dep': 'Dependent variable'
267
+ }).props('inline').bind_value(
268
+ self.sm.active_expt_data.col_details[col], 'depindep'
269
+ )
270
+ self.dep_indep_dropdowns.append(dep_indep_radio)
271
+
272
+ # Data type dropdown
273
+ with ui.row().classes('items-center gap-2'):
274
+ dtype_label = ui.label("Data type:").classes("text-sm").props('inline')
275
+ self.dtype_labels.append(dtype_label)
276
+
277
+ opts = {k: v.name for k,v in self.sm._expt_dtypes.items()}
278
+ dtype_select = ui.select(opts,label="Data type").props('inline').bind_value(
279
+ self.sm.active_expt_data.col_details[col], 'dtype'
280
+ )
281
+ dtype_select.classes('w-40')
282
+ self.dtype_dropdowns.append(dtype_select)
283
+
284
+ # Set up column selection functionality
285
+ def setup_selection_behavior(card, dep_radio, dtype_sel, dtype_lbl, include_checkbox, col_name):
286
+ def update_selection_state(notify_listeners=True):
287
+ is_selected = include_checkbox.value
288
+ if is_selected:
289
+ # Add to selected columns if not already there
290
+ if col_name not in self.sm.active_expt_data.selected_columns:
291
+ self.sm.active_expt_data.selected_columns.append(col_name)
292
+ # Enable controls
293
+ card.classes(remove="opacity-50")
294
+ dep_radio.set_enabled(True)
295
+ dtype_sel.set_enabled(True)
296
+ else:
297
+ # Remove from selected columns
298
+ if col_name in self.sm.active_expt_data.selected_columns:
299
+ self.sm.active_expt_data.selected_columns.remove(col_name)
300
+ # Grey out and clear other controls
301
+ card.classes("opacity-50")
302
+ dep_radio.set_enabled(False)
303
+ dtype_sel.set_enabled(False)
304
+ # Clear assignments when deselected
305
+ self.sm.active_expt_data.col_details[col_name]['depindep'] = None
306
+ self.sm.active_expt_data.col_details[col_name]['dtype'] = None
307
+
308
+ # Only notify listeners if not during initial setup
309
+ if notify_listeners:
310
+ self.sm.notify_listeners("expt_data_columns_changed")
311
+
312
+ # Update state when checkbox changes (with notification)
313
+ include_checkbox.on_value_change(lambda: update_selection_state(True))
314
+ # Set initial state without triggering events
315
+ update_selection_state(False)
316
+
317
+ setup_selection_behavior(card, dep_indep_radio, dtype_select, dtype_label, include_cb, col)
318
+
319
+ with ui.row():
320
+ ui.button("Add new data type", on_click=self.add_new_expt_data_type).props(
321
+ "unelevated color=primary"
322
+ ).classes("q-mx-xs")
323
+ else:
324
+ ui.label("No columns available in experimental data.")
325
+
326
+
327
+ async def add_new_expt_data_type(self):
328
+ """Add a new experimental data type."""
329
+ with ui.dialog() as dialog, ui.card():
330
+ ui.label("Add New Experimental Data Type")
331
+ name_input = ui.input("Name", placeholder="Enter data type name").props("clearable")
332
+ with ui.row().classes('items-center gap-2'):
333
+ ui.label("Measurement Method")
334
+ meas_input = ui.select(label="Measurement Method",
335
+ options={'grav_vol': "Grav/volumetric", 'nmr_integ': "NMR integration",
336
+ 'nmr_ppm': "NMR chemical shift", 'uv_abs': "UV-vis",
337
+ 'fluorescence': "Fluorescence"}).props(
338
+ "clearable").on_value_change(lambda e: method_changed(e.value)).classes('w-40')
339
+
340
+ with ui.row().classes('items-center gap-2'):
341
+ ui.label("Units")
342
+ units_input = ui.select(label="Units", options=["ppm", "M", "mM", "uM", "nM", "absorbance", "intensity"]).props("clearable").classes('w-15')
343
+
344
+ variance_input = ui.number("Variance", placeholder="Enter variance",min=1e-20).props("clearable").tooltip("Give the variance in the data, using the same units as the measurements. Default: 0.005")
345
+
346
+ def method_changed(value):
347
+ if value == "nmr_ppm":
348
+ units_input.value = "ppm"
349
+ elif value in ["grav_vol", "nmr_integ"]:
350
+ units_input.value = "M"
351
+ elif value == "uv_abs":
352
+ units_input.value = "absorbance"
353
+ elif value == "fluorescence":
354
+ units_input.value = "intensity"
355
+
356
+
357
+ def on_submit():
358
+ if name_input.value and meas_input.value and units_input.value:
359
+ lnsigma_centre = float(math.log(variance_input.value)) if variance_input.value else -5
360
+ lnsigma = (lnsigma_centre - 3, lnsigma_centre, lnsigma_centre + 3)
361
+ new_dtype = ExptDataType(
362
+ name=name_input.value,
363
+ init_meas=meas_input.value,
364
+ units=units_input.value,
365
+ lnsigma= lnsigma[1],
366
+ lnsigma_min=lnsigma[0],
367
+ lnsigma_max=lnsigma[2]
368
+ )
369
+ try:
370
+ self.sm.add_expt_data_type(new_dtype)
371
+ except ValueError as e:
372
+ ui.notify(str(e), type="negative")
373
+ dialog.submit(True)
374
+ else:
375
+ ui.notify("Please fill in all fields.", type="negative")
376
+
377
+ ui.button("Submit", on_click=on_submit).props("unelevated color=primary")
378
+ ui.button("Cancel", on_click=lambda: dialog.close()).props(
379
+ "unelevated color=secondary"
380
+ )
381
+ result = await dialog
382
+ if result:
383
+ ui.notify(f"New experimental data type '{name_input.value}' added.", type="positive")
384
+ self._load_expt_data_col_details()
385
+
386
+ def generate_data_dropdown(self, e=None):
387
+ """Generate the dropdown for selecting experimental data."""
388
+
389
+ self.expt_data_dropdown_button.clear()
390
+ if len(self.sm.expt_datas) >0 and len(self.sm.raw_datas) >0:
391
+ self.expt_data_dropdown_button.visible = True
392
+ with self.expt_data_dropdown_button:
393
+ self.expt_data_dropdown_rows = []
394
+ for m in self.sm.raw_datas.values():
395
+ with ui.row().classes("p-5 items-center") as x:
396
+ self.expt_data_dropdown_rows.append(x)
397
+ with ui.item(on_click=lambda m=m: self.load_raw_data(m)):
398
+ ui.item_label(m.filename)
399
+ ui.icon("delete").on(
400
+ "click", lambda m=m: self.delete_raw_data(m)
401
+ ).classes("cursor-pointer text-red-600")
402
+ #ui.item("Add a new model...", on_click= self.add_new_expt_data)
403
+ else:
404
+ self.expt_data_dropdown_button.visible = False
405
+ active_raw = self.sm.active_raw_data_or_none
406
+ if active_raw is not None and hasattr(active_raw, 'filename'):
407
+ self.expt_data_dropdown_button.bind_text_from(active_raw, "filename")
408
+
409
+
410
+
411
+ def load_raw_data(self, raw_data):
412
+ self.sm.active_raw_data_id = raw_data.id
413
+ active_expt = self.sm.active_expt_data_or_none
414
+ if active_expt is None or active_expt.raw_data_id != raw_data.id:
415
+ self.sm.active_expt_data_id = None # reset active expt data if raw data changes
416
+ # activate the newest expt data that uses this raw data
417
+ for ed in reversed(list(self.sm.expt_datas.values())):
418
+ if ed.raw_data_id == raw_data.id:
419
+ self.sm.active_expt_data_id = ed.id
420
+ break
421
+
422
+ active_fit = self.sm.active_fit_or_none
423
+ if active_fit is None or active_fit.expt_data_id != self.sm.active_expt_data_id:
424
+ self.sm.active_fit_id = None # reset active fit if expt data changes
425
+ # activate the newest fit that uses this expt data
426
+ for f in reversed(list(self.sm.fits.values())):
427
+ if f.expt_data_id == self.sm.active_expt_data_id:
428
+ self.sm.active_fit_id = f.id
429
+ break
430
+
431
+ self.sm.reconcile_active_context(reason="load_raw_data_selection", emit_events=True)
432
+ self.sm.notify_listeners("data_imported")
433
+
434
+ def delete_raw_data(self, raw_data):
435
+ self.sm.delete_raw_data(raw_data)
436
+ # objs_to_delete = []
437
+ # for f in self.sm.expt_datas.values():
438
+ # if f.raw_data_id == raw_data.id:
439
+ # objs_to_delete.append(f)
440
+ # for ff in self.sm.fits.values():
441
+ # if ff.expt_data_id == f.id:
442
+ # objs_to_delete.append(ff)
443
+ # for obj in objs_to_delete:
444
+ # self.sm.delete_object(obj)
445
+
446
+
447
+
448
+
449
+ def add_new_raw_data(self):
450
+ pass