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,419 @@
1
+ from .base import BaseComponent
2
+ from nicegui import ui
3
+ import numpy as np
4
+ import pandas as pd
5
+ from ..classes import Component
6
+ from .graph import Graph
7
+
8
+ from nicegui.events import UploadEventArguments
9
+ from collections import Counter
10
+ import io
11
+
12
+ class DataGenerationPanel(BaseComponent):
13
+ def _create_comp_table(self, df: pd.DataFrame) -> None:
14
+ self.compTable = (
15
+ ui.table.from_pandas(
16
+ df,
17
+ column_defaults={
18
+ "align": "right",
19
+ ":format": """value => {
20
+ if (value == null) return ''
21
+ const v = Number(value)
22
+
23
+ if (Math.abs(v) < 1e-16) return '0'
24
+
25
+ return (Math.abs(v) >= 1e4 || Math.abs(v) < 1e-3)
26
+ ? v.toExponential(3)
27
+ : v.toFixed(3)
28
+ }""",
29
+ },
30
+ )
31
+ .classes("w-full")
32
+ .props('dense hide-bottom :pagination="{rowsPerPage: 0}"')
33
+ .mark("gen-data-table")
34
+ )
35
+
36
+ def setup_nicegui(self):
37
+ self.container = ui.column().classes("w-full")
38
+
39
+ with self.container:
40
+ # Responsive layout:
41
+ # - narrow screens: stack cards top-to-bottom
42
+ # - wide screens: show cards side-by-side (generation left, table/graph right)
43
+ with ui.row().classes("w-full gap-4 items-start flex-col lg:flex-row"):
44
+ with ui.card().classes("w-full lg:flex-1 min-w-0"):
45
+ self.gen_settings = ui.element()
46
+ with self.gen_settings:
47
+ ui.label("Data Generation Panel").classes("text-lg font-bold mb-4")
48
+ ui.label("This panel contains data generation tools and options.")
49
+ with ui.row():
50
+ self.nCompInp = (
51
+ ui.input("Number of components")
52
+ .bind_value(self.sm, "nComp")
53
+ .mark("num-components")
54
+ .set_enabled(False)
55
+ ) # .on_value_change(self.simUpdateNumComponents)
56
+ # self.nCompEditable = ui.checkbox("",value=False).props('checked-icon=edit unchecked-icon=edit_off').classes('q-pa-md')
57
+ # self.nCompInp.bind_enabled_from(self.nCompEditable,'value')
58
+ # change this to be a nice pen icon that is crossed out
59
+ self.nStepInp = (
60
+ ui.number("Number of steps", precision=0, min=1)
61
+ .mark("num-steps")
62
+ .bind_value(self.sm, "nStep")
63
+ )
64
+ ui.button("Upload component concentrations", on_click=self.upload_comp_concs)
65
+ # Wrap component blocks left-to-right, then top-to-bottom
66
+ self.comp_gens = ui.row().classes("w-full flex-wrap items-start gap-4")
67
+
68
+ self.set_up_component_gen_rows(self.sm.nComp)
69
+ with ui.row().classes("mt-4 gap-4"):
70
+ ui.button(
71
+ "Generate Component Concentrations", on_click=self.calc_comp_concs
72
+ )
73
+ ui.button("Reset", on_click=self.resetcomp_concs)
74
+
75
+ with ui.card().classes("w-full lg:flex-1 min-w-0"):
76
+ self.table_and_graph = ui.element().classes("w-full")
77
+ with self.table_and_graph:
78
+ with (
79
+ ui.tabs()
80
+ .classes("w-full")
81
+ .props('align="justify"')
82
+ .on("update:model-value", self._table_graph_tab_changed)
83
+ ) as tabs:
84
+ self.table_tab = ui.tab("Table")
85
+ self.graph_tab = ui.tab("Graph")
86
+ with ui.tab_panels(tabs, value=self.table_tab).classes("w-full"):
87
+ with ui.tab_panel(self.table_tab):
88
+ self.tableBlock = ui.element().classes("w-full")
89
+ with self.tableBlock:
90
+ ui.label("Generated data:")
91
+ with (
92
+ ui.element("div")
93
+ .classes("w-full max-h-[30rem] overflow-y-scroll")
94
+ .style("scrollbar-gutter: stable;")
95
+ ):
96
+ self._create_comp_table(self.sm.comp_concs)
97
+
98
+ with ui.tab_panel(self.graph_tab):
99
+ ui.label("Generated data:")
100
+ self.preview_graph = Graph(self.sm, mode="data_preview")
101
+ if len(self.sm.comp_concs) > 0:
102
+ self.preview_graph.add_graph_lines(self.sm.comp_concs, run_name="Gen", run_id=str(self.sm.active_model_id)) # Add graph lines for the generated data
103
+ self.preview_graph.update_graph()
104
+
105
+
106
+ def _table_graph_tab_changed(self, e) -> None:
107
+ # Plotly elements often compute width incorrectly when created inside a hidden tab.
108
+ # Forcing an update when the Graph tab becomes active makes it fill the card width.
109
+ if getattr(e, "args", None) != "Graph":
110
+ return
111
+ if not hasattr(self, "preview_graph"):
112
+ return
113
+ try:
114
+ self.preview_graph.graph.update()
115
+ except Exception:
116
+ pass
117
+ try:
118
+ ui.run_javascript('window.dispatchEvent(new Event("resize"));')
119
+ except Exception:
120
+ pass
121
+
122
+
123
+
124
+
125
+ async def upload_comp_concs(self, e):
126
+
127
+ with ui.dialog() as dialog, ui.card():
128
+ ui.label("Upload concentrations (csv)").classes("w-full text-center text-2xl font-bold")
129
+ template_text = ','.join([comp.name for comp in self.sm.components])
130
+ template_text += '\nmM'+',mM'*(len(self.sm.components)-1) # Header row with units
131
+ template_text += '\n' + '0.001' + ',0'*(len(self.sm.components)-1) # Add a newline after the header
132
+
133
+
134
+ ui.button(
135
+ "Download template CSV",
136
+ on_click=lambda: ui.download.content(template_text, 'component_concentrations_template.csv')
137
+ )
138
+
139
+ ui.label("""
140
+ 1. Download the template CSV file and open it in Excel or your preferred spreadsheet software.
141
+ 2. The first row contains the component names. Do not change this row.
142
+ 3. The second row contains the units for each component (M, mM, µM/uM, nM). Ensure these are correct, and change them if needed.
143
+ 4. Starting from the third row, enter the concentration values for each component. You can change the existing data.
144
+ 5. Save as a CSV, then upload your file by pressing the (+) button below.""").style('white-space: pre-wrap')
145
+ upload_box = ui.upload(label="Choose file", auto_upload=True).props(
146
+ 'accept=".csv"'
147
+ )
148
+
149
+ ui.button("Cancel", on_click=lambda: dialog.submit("cancel"))
150
+
151
+ def on_upload_complete(e: UploadEventArguments) -> None:
152
+ dialog.submit(e) # Store result for later
153
+
154
+ upload_box.on_upload(on_upload_complete)
155
+
156
+ result: UploadEventArguments|str= await dialog
157
+
158
+ if isinstance(result, str) and result == "cancel":
159
+ ui.notify("Project loading cancelled", type="info")
160
+ return
161
+
162
+ elif isinstance(result, UploadEventArguments):
163
+ filename = result.file.name
164
+
165
+ if filename.endswith(".csv"):
166
+ file_content = await result.file.text("utf-8")
167
+ else:
168
+ ui.notify("Unsupported file format. Please upload a .csv file.",type='negative')
169
+ return
170
+ concs = pd.read_csv(io.StringIO(file_content),header=0,index_col=False,skiprows=[1])
171
+ units = file_content.splitlines()[1].split(',')
172
+
173
+ # check names are all valid
174
+ expected_names = [comp.name for comp in self.sm.components]
175
+ if Counter(concs.columns) != Counter(expected_names):
176
+ ui.notify(f"Component names in uploaded file do not match expected names: {', '.join(expected_names)}",type='negative')
177
+ return
178
+
179
+ # check units are all valid
180
+ valid_units = ['M','mM','uM','µM','nM']
181
+ for unit in units:
182
+ if unit not in valid_units:
183
+ ui.notify(f"Invalid unit '{unit}' found in uploaded file. Valid units are: {', '.join(valid_units)}",type='negative')
184
+ return
185
+
186
+ # do unit conversion to M
187
+ for i, comp in enumerate(concs.columns):
188
+ unit = units[i]
189
+ if unit == 'M':
190
+ factor = 1
191
+ elif unit == 'mM':
192
+ factor = 1e-3
193
+ elif unit in ['uM','µM']:
194
+ factor = 1e-6
195
+ elif unit == 'nM':
196
+ factor = 1e-9
197
+ else:
198
+ factor = 1 # should not happen due to earlier check
199
+ concs[comp] = concs[comp] * factor
200
+
201
+ self.sm.comp_concs = concs
202
+ self.sm.active_model.component_concs = concs
203
+ self.update_comp_table()
204
+
205
+
206
+
207
+
208
+ def calc_comp_concs(self, e):
209
+ """Calculate component concentrations based on user input."""
210
+
211
+ # remove components that are no longer in the list
212
+ compNames = [comp.name for comp in self.sm.components]
213
+ self.sm.components = [
214
+ comp for comp in self.sm.components if comp.name in compNames
215
+ ]
216
+
217
+ df = pd.DataFrame({})
218
+ try:
219
+ for comp in self.sm.components:
220
+ if comp.constant is True:
221
+ df[comp.name] = [comp.start_conc] * int(self.sm.nStep)
222
+
223
+ else:
224
+ if comp.spacing == "lin":
225
+ df[comp.name] = np.linspace(
226
+ comp.start_conc if comp.start_conc is not None else 0,
227
+ comp.end_conc if comp.end_conc is not None else 0,
228
+ int(self.sm.nStep),
229
+ )
230
+
231
+ elif comp.spacing == "log":
232
+ pass
233
+ except Exception as e:
234
+ if isinstance(e, TypeError):
235
+ ui.notify(
236
+ "Error in component concentration calculation. Most likely you have not provided start/end concentrations. "
237
+ + str(e),
238
+ type="negative",
239
+ )
240
+ return
241
+
242
+ self.sm.comp_concs = df
243
+ self.sm.active_model.component_concs = df # Update the active model's component concentrations # TODO make this a getter setter deal
244
+ # self.preview_graph.add_graph_lines(df, run_name="Gen", run_id=str(self.sm.active_model_id)) # Add graph lines for the generated data
245
+
246
+ # self.preview_graph.update_graph()
247
+ self.update_comp_table()
248
+
249
+ self.sm.notify_listeners("comp_concs_updated")
250
+ # self.compTable.clear()
251
+ # self.compTable = ui.table.from_pandas(df) # make it pretty - rounding, units, spacing reduction.
252
+
253
+ def update_comp_table(self):
254
+ self.tableBlock.clear()
255
+ with self.tableBlock:
256
+ ui.label("Generated data:")
257
+ with (
258
+ ui.element("div")
259
+ .classes("w-full max-h-[30rem] overflow-y-scroll")
260
+ .style("scrollbar-gutter: stable;")
261
+ ):
262
+ self._create_comp_table(self.sm.comp_concs)
263
+
264
+ self.preview_graph.clear_graph() # Clear existing graph data
265
+ self.preview_graph.add_graph_lines(self.sm.comp_concs, run_name="Gen", run_id=str(self.sm.active_model_id)) # Add graph lines for the generated data
266
+ self.preview_graph.update_graph()
267
+
268
+ self.sm.notify_listeners("comp_concs_updated")
269
+
270
+ def resetcomp_concs(self, e):
271
+ self.comp_concs = pd.DataFrame({})
272
+ self.set_up_component_gen_rows(
273
+ self.sm.nComp
274
+ ) # Reset component generation rows based on the number of components
275
+
276
+ # save states e.g. initial, after simrun, etc?
277
+ # history/undo options?
278
+
279
+ def setup_bindings(self):
280
+ self.sm.add_listener("model_changed", self.regen_comps)
281
+
282
+ def regen_comps(self, e=None):
283
+ self.set_up_component_gen_rows(
284
+ len(self.sm.components)
285
+ ) # Update component generation rows based on the model's components
286
+ self.tableBlock.clear()
287
+ with self.tableBlock:
288
+ ui.label("Generated data:")
289
+ with (
290
+ ui.element("div")
291
+ .classes("w-full max-h-[30rem] overflow-y-scroll")
292
+ .style("scrollbar-gutter: stable;")
293
+ ):
294
+ self._create_comp_table(self.sm.comp_concs)
295
+
296
+ def set_up_component_gen_rows(self, n: int):
297
+ """Set up the component generation rows based on the number of components."""
298
+
299
+ self.comp_gens.clear()
300
+
301
+ for ii in range(n):
302
+ with self.comp_gens:
303
+ if ii < len(self.sm.components):
304
+ # If the component already exists, use its data
305
+ self.addCompBox(
306
+ ii,
307
+ name=self.sm.components[ii].name,
308
+ constant=self.sm.components[ii].constant,
309
+ start=self.sm.components[ii].start_conc_nice,
310
+ startunit=self.sm.components[ii].start_units,
311
+ end=self.sm.components[ii].end_conc_nice,
312
+ endunit=self.sm.components[ii].end_units,
313
+ )
314
+ else:
315
+ # If the component does not exist, create a new one with default values
316
+ # This allows for adding new components without losing existing ones
317
+ self.sm.components.append(
318
+ Component(name=f"Component {ii+1}")
319
+ ) # Add a new component to the list
320
+ self.addCompBox(ii)
321
+
322
+ if len(self.sm.components) > n:
323
+ # If there are more components than requested, remove the excess
324
+ self.sm.components = self.sm.components[:n]
325
+
326
+ def addCompBox(
327
+ self,
328
+ n,
329
+ name="",
330
+ constant=False,
331
+ start=None,
332
+ startunit="mM",
333
+ end=None,
334
+ endunit="mM",
335
+ new=False,
336
+ ):
337
+
338
+ c = ui.card().classes("w-full sm:w-56 md:w-64 flex-none")
339
+ with c:
340
+ ui.label(f"Component {n+1}:").classes("w-full text-center text-lg font-bold")
341
+ ui.input("Name").mark(f"comp-name-{n+1}").bind_value(
342
+ self.sm.components[n], "name"
343
+ )
344
+ ui.checkbox(
345
+ "Constant conc?", value=constant, on_change=self.changeConstantConc
346
+ ).mark(f"constant-conc-{n+1}-checkbox").bind_value(
347
+ self.sm.components[n], "constant"
348
+ )
349
+ with ui.row().classes("start-conc-row"):
350
+ ui.number("Start:", step=0.001, value=start).classes("w-24").mark(
351
+ f"start-conc-{n+1}-val"
352
+ ).bind_value(self.sm.components[n], "start_conc_nice")
353
+ ui.select(["M", "mM", "µM", "nM"], value=startunit).classes(
354
+ "w-20"
355
+ ).mark(f"start-conc-{n+1}-unit").bind_value(
356
+ self.sm.components[n], "start_units"
357
+ )
358
+ with ui.row().classes("end-conc-row"):
359
+ ui.number("End:", step=0.001, value=end).classes("w-24").mark(
360
+ f"end-conc-{n+1}-val"
361
+ ).bind_value(self.sm.components[n], "end_conc_nice")
362
+ ui.select(["M", "mM", "µM", "nM"], value=endunit).classes("w-20").mark(
363
+ f"end-conc-{n+1}-unit"
364
+ ).bind_value(self.sm.components[n], "end_units")
365
+
366
+ return c
367
+
368
+ def getEventSiblings(self, e):
369
+ """Get all siblings of the event sender."""
370
+ compBlock = next(e.sender.ancestors())
371
+ return compBlock.default_slot.children
372
+
373
+ def changeConstantConc(self, e):
374
+ """Handle changes to the constant concentration checkbox."""
375
+
376
+ # Store references to UI elements when creating them instead of trying to find them later
377
+ # For now, just print the checkbox state as a placeholder
378
+ els = self.getEventSiblings(e)
379
+
380
+ if e.value:
381
+ for el in els:
382
+
383
+ if isinstance(el, ui.row):
384
+ # these are start/end concentration rows
385
+ inStartRow = True
386
+ for child in el.default_slot.children:
387
+ if isinstance(child, ui.number) and child.label == "Start:":
388
+ inStartRow = True
389
+ child.label = "Conc:"
390
+ elif isinstance(child, ui.number) and child.label == "End:":
391
+ inStartRow = False
392
+ child.value = None
393
+ child.enabled = False
394
+ elif isinstance(child, ui.select) and not inStartRow:
395
+ child.value = "mM"
396
+ child.enabled = False
397
+
398
+ else:
399
+ self.resetCompConcBlock(els)
400
+
401
+ def resetCompConcBlock(self, els):
402
+ """Reset the concentration block for a component."""
403
+ """Takes a list of elements, which should comprise a component block."""
404
+ for el in els:
405
+
406
+ if isinstance(el, ui.row):
407
+ # these are start/end concentration rows
408
+ inStartRow = True
409
+ for child in el.default_slot.children:
410
+ if isinstance(child, ui.number) and child.label == "Conc:":
411
+ inStartRow = True
412
+ child.label = "Start:"
413
+ elif isinstance(child, ui.number) and child.label == "End:":
414
+ inStartRow = False
415
+ child.value = None
416
+ child.enabled = True
417
+ elif isinstance(child, ui.select) and not inStartRow:
418
+ child.value = "mM"
419
+ child.enabled = True