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,330 @@
1
+ from .base import BaseComponent
2
+ from nicegui import ui,binding
3
+ from ..utils import (
4
+ get_components_from_species,
5
+ )
6
+ from ..state.statemanager import StateManager
7
+
8
+
9
+ class BindingModelPanel(BaseComponent):
10
+ def __init__(self, state_manager: StateManager, mode="sim"):
11
+ self.mode = mode
12
+ super().__init__(state_manager)
13
+ self.generate_eq_const_rows()
14
+
15
+ def setup_bindings(self):
16
+ """Set up data bindings and listeners."""
17
+
18
+ self.sm.add_listener("model_changed", self.generate_eq_const_rows)
19
+ self.sm.add_listener("model_changed", self.generate_models_dropdown)
20
+ self.sm.add_listener("model_changed", self.refresh_ui_bindings)
21
+
22
+
23
+ def refresh_ui_bindings(self,e=None):
24
+ """Refresh UI bindings after model changes."""
25
+ # Check if UI elements exist before trying to bind them
26
+ if not hasattr(self, 'model_name_inp') or self.model_name_inp is None:
27
+ return
28
+
29
+ binding.remove([self.model_name_inp,
30
+ self.eqn_inp,
31
+ self.model_dropdown_button,
32
+ self.modeleq_mat,
33
+ self.modelComp,
34
+ self.modelSpec
35
+ ])
36
+
37
+ self.model_name_inp.bind_text_from(self.sm.active_model, "name")
38
+ self.eqn_inp.bind_value(self.sm.active_model, "eq_str")
39
+ self.model_dropdown_button.bind_text_from(self.sm.active_model,"name")
40
+ if len(self.sm.models) > 1 or len(self.sm.active_model.eq_mat_str) > 1:
41
+ self.modelData.set_visibility(True)
42
+ self.modeleq_mat.bind_value_from(self.sm.active_model, "eq_mat_str")
43
+ self.modelComp.bind_value_from(self.sm.active_model, "component_names")
44
+ self.modelSpec.bind_value_from(self.sm.active_model, "species")
45
+ else:
46
+ self.modelData.set_visibility(False)
47
+
48
+ # if self.sm.active_model_id not in self.sm.default_model_ids:
49
+ # self.edit_name_btn.props("flat").classes("ml-2").tooltip("Rename model").on('click', self.show_rename_dialog)
50
+ # else:
51
+ # self.edit_name_btn.props("flat").classes("ml-2").tooltip("Cannot rename default model")
52
+
53
+
54
+ def show_model_data(self, e):
55
+ """Show the model data output area when a model is parsed."""
56
+ self.modelData.set_visibility(True) # Show the model data output area
57
+
58
+ def setup_nicegui(self):
59
+ mode = self.mode
60
+ self.container = ui.column().classes("w-full")
61
+
62
+ with self.container:
63
+ with ui.row().classes("w-full"):
64
+ self.model_dropdown_button = (
65
+ ui.dropdown_button("Choose model", auto_close=True)
66
+ .bind_text_from(self.sm.active_model, "name")
67
+ .classes("mb-5")
68
+ )
69
+ self.generate_models_dropdown()
70
+ ui.button("Add New Model", on_click=lambda e: self.add_new_model()).classes("mb-5")
71
+
72
+ with ui.row().classes("w-full mb-4"):
73
+ with ui.row().classes("w-full items-center"):
74
+ ui.label("Model Name:").style("font-weight: bold")
75
+
76
+ self.model_name_inp = (
77
+ ui.label("Model Name")
78
+ .bind_text_from(self.sm.active_model, "name")
79
+ .tooltip(
80
+ "This name will appear in the legends of any figures. Make it descriptive but short! You might mention key details about the model."
81
+ )
82
+ )
83
+ # if self.sm.active_model_id not in self.sm.default_model_ids:
84
+ # self.edit_name_btn = ui.button(icon="edit", on_click=self.show_rename_dialog).props("flat").classes("ml-2").tooltip("Rename model")
85
+ # else:
86
+ # self.edit_name_btn = ui.button(icon="edit_off").props("flat").classes("ml-2").tooltip("Cannot rename default model")
87
+
88
+ self.eqn_inp = (
89
+ ui.textarea("Equilibrium Equations", placeholder="H+G<=>HG")
90
+ .bind_value(self.sm.active_model, "eq_str")
91
+ .classes("w-full mb-4")
92
+ .tooltip(
93
+ "Use the format H+G=HG. Separate equations with semicolons or new lines."
94
+ )
95
+ .props("clearable rows=2 autogrow")
96
+ .mark("eq-input")
97
+ )
98
+
99
+ ui.button("Parse Equations", on_click=self.sm.parse_equations).classes("mb-4")
100
+
101
+ self.eqConstRows = ui.row().classes('w-full')
102
+ with self.eqConstRows:
103
+ ui.label("Equilibrium Constants:")
104
+ self.logKchk = (
105
+ ui.checkbox("Use log scale for constants", value=True)
106
+ .classes("mb-2")
107
+ .mark("log-scale-checkbox")
108
+ ) # Checkbox to toggle log scale
109
+ self.logKchk.set_enabled(False)
110
+ if len(self.sm.active_model.binding_constants) > 0:
111
+ self.generate_eq_const_rows(
112
+ mode
113
+ ) # Generate equilibrium constants input rows if binding constants exist
114
+ else:
115
+ self.eqConstRows.set_visibility(
116
+ False
117
+ ) # Placeholder for equilibrium constants input
118
+
119
+ self.modelData = ui.element("div").mark(
120
+ "model-setup-output"
121
+ ) # Placeholder for model setup output
122
+ self.advDetails = ui.expansion("Advanced model details")
123
+ # if the model already exists, show the constants/etc block
124
+ if len(self.sm.models) > 1 or len(self.sm.active_model.eq_mat_str) > 1:
125
+ self.modelData.set_visibility(True) # Initially hidden
126
+ else:
127
+ self.modelData.set_visibility(False) # Initially hidden
128
+
129
+ with self.modelData:
130
+ with self.advDetails:
131
+ self.modeleq_mat = (
132
+ ui.textarea("Equation matrix")
133
+ .props("rows=1 autogrow")
134
+ .classes("w-full mb-4")
135
+ .bind_value(self.sm.active_model, "eq_mat_str")
136
+ .mark("eq-matrix-output")
137
+ )
138
+ self.modelComp = (
139
+ ui.input("Components", value="")
140
+ .classes("w-full mb-4")
141
+ .mark("components-output")
142
+ .bind_value(self.sm.active_model, "component_names")
143
+ )
144
+ self.modelSpec = (
145
+ ui.input("Species", value="")
146
+ .classes("w-full mb-4")
147
+ .mark("species-output")
148
+ .bind_value(self.sm.active_model, "species")
149
+ )
150
+
151
+ self.modeleq_mat.ignores_events_when_disabled = False
152
+ self.modeleq_mat.disable()
153
+ self.modelComp.ignores_events_when_disabled = False
154
+ self.modelComp.disable()
155
+ self.modelSpec.ignores_events_when_disabled = False
156
+ self.modelSpec.disable()
157
+
158
+ def generate_models_dropdown(self, e=None):
159
+ """Generate the dropdown for model selection."""
160
+ self.model_dropdown_button.clear()
161
+ with self.model_dropdown_button:
162
+ self.model_dropdown_rows = []
163
+ for i,m in enumerate(self.sm.models.values()):
164
+ with ui.row().classes("p-1 items-center justify-between w-full no-wrap") as x:
165
+ self.model_dropdown_rows.append(x)
166
+ with ui.item(on_click=lambda m=m: self.load_model(m)).classes("flex-grow min-w-0 flex items-center"):
167
+ ui.item_label().bind_text(
168
+ m, "name"
169
+ ).classes("leading-none")
170
+
171
+ if m.id not in self.sm.default_model_ids: # default model IDs
172
+ ui.icon("delete").on(
173
+ "click", lambda m=m: self.delete_model(m)
174
+ ).classes("cursor-pointer text-red-600 flex-shrink-0 self-center").tooltip("Delete model")
175
+ ui.icon("edit").on("click", lambda m=m: self.show_rename_dialog(m)).classes("cursor-pointer flex-shrink-0 self-center").tooltip("Rename model")
176
+ else:
177
+ ui.icon("lock").classes("text-gray-600 flex-shrink-0 self-center").tooltip("Built-in model")
178
+ ui.icon("edit_off").classes("text-gray-600 flex-shrink-0 self-center").tooltip("Cannot rename default model")
179
+
180
+ # if self.sm.active_model_id not in self.sm.default_model_ids:
181
+ # self.edit_name_btn = ui.button(icon="edit", on_click=self.show_rename_dialog).props("flat").classes("ml-2").tooltip("Rename model")
182
+ # else:
183
+ # self.edit_name_btn = ui.button(icon="edit_off").props("flat").classes("ml-2").tooltip("Cannot rename default model")
184
+ def delete_model(self, m):
185
+ self.sm.delete_model(m)
186
+
187
+ async def add_new_model(self):
188
+ async def show_name_dialog():
189
+ with ui.dialog() as dialog, ui.card():
190
+ ui.label('Enter name for new model:')
191
+ name_input = ui.input('Model name', placeholder='Enter model name')
192
+ with ui.row():
193
+ ui.button('Cancel', on_click=dialog.close)
194
+ ui.button('Create', on_click=lambda: create_model_with_name(name_input.value))
195
+
196
+ def create_model_with_name(name):
197
+ def add_model():
198
+ self.sm.new_model(name.strip())
199
+ self.modelData.set_visibility(False) # Hide the model data output area
200
+ self.eqConstRows.set_visibility(False)
201
+ self.sm.notify_listeners("model_changed") # Notify listeners to update the UI
202
+ dialog.close()
203
+ if name.strip():
204
+ if name.strip() in [m.name for m in self.sm.models.values()]:
205
+ with ui.dialog() as confirm_dialog, ui.card():
206
+ ui.label(f'A model named "{name.strip()}" already exists.')
207
+ ui.label('Would you like to overwrite it or choose a different name?')
208
+ with ui.row():
209
+ ui.button('Choose Different Name', on_click=lambda: (confirm_dialog.close(), show_name_dialog()))
210
+ ui.button('Overwrite', on_click=lambda: add_model())
211
+ confirm_dialog.open()
212
+ return
213
+ else:
214
+ add_model()
215
+
216
+ dialog.open()
217
+
218
+ await show_name_dialog()
219
+
220
+ async def show_rename_dialog(self,m):
221
+ async def show_name_dialog():
222
+ with ui.dialog() as dialog, ui.card():
223
+ ui.label('Enter new name for model:')
224
+ name_input = ui.input('Model name', placeholder='Enter model name', value=m.name)
225
+ with ui.row():
226
+ ui.button('Cancel', on_click=dialog.close)
227
+ ui.button('Rename', on_click=lambda: rename_model_to(name_input.value))
228
+
229
+ def rename_model_to(name):
230
+ if name.strip() and name.strip() != m.name:
231
+ if name.strip() in [m.name for m in self.sm.models.values()]:
232
+ with ui.dialog() as confirm_dialog, ui.card():
233
+ ui.label(f'A model named "{name.strip()}" already exists.')
234
+ ui.label('Please choose a different name.')
235
+ with ui.row():
236
+ ui.button('OK', on_click=confirm_dialog.close)
237
+ confirm_dialog.open()
238
+ return
239
+ m.name = name.strip()
240
+ self.sm.notify_listeners("model_changed") # Notify listeners to update the UI
241
+ dialog.close()
242
+
243
+ dialog.open()
244
+
245
+ await show_name_dialog()
246
+
247
+
248
+ def load_model(self, m):
249
+ """Load an existing model and update the UI accordingly."""
250
+ self.sm.active_model_id = m.id
251
+ self.sm.notify_listeners("model_changed")
252
+
253
+ def generate_eq_const_rows(self, mode="sim"):
254
+ """Generate the equilibrium constants input rows."""
255
+ # Re-generate equilibrium constants rows
256
+ if len(self.sm.active_model.binding_constants) < 1:
257
+ if len(self.sm.active_model.species)>0:
258
+ # If no binding constants exist, create a new one
259
+ self.sm.generate_binding_constants()
260
+ else:
261
+ self.eqConstRows.set_visibility(False)
262
+ return
263
+
264
+
265
+ self.eqConstRows.set_visibility(
266
+ True
267
+ ) # Show the equilibrium constants input area
268
+
269
+ if self.logKchk.value is True:
270
+ logtxt = r"\\log"
271
+ else:
272
+ logtxt = ""
273
+ # populate binding constant definition section
274
+ with self.eqConstRows as block:
275
+
276
+ block.clear() # Clear previous content
277
+ # now set up binding constant blocks either with existing bound content or with the newly-instantiated
278
+ # BindingConstant objects
279
+ for i, b in enumerate(self.sm.active_model.binding_constants):
280
+ # if species is has no existing BC, add one
281
+ if b.isComp:
282
+ continue
283
+
284
+ bspecies = b.species
285
+ comps = get_components_from_species(bspecies)
286
+ # i_in_full_list = self.sm.active_model.species.index(bspecies)
287
+
288
+ # comps will have format [A,B,B] for A + 2B
289
+ # so convert to a string like 'A + 2B'
290
+ with ui.card().classes("mb-2 w-72").mark(f"eq-const-row-{i+1}"):
291
+ with ui.row(align_items='center').classes('w-full justify-center'):
292
+ ui.label(
293
+ "{} ⇋ {}".format(
294
+ " + ".join(
295
+ [
296
+ f"{comps.count(c) if comps.count(c) > 1 else ''}{c}"
297
+ for c in list(dict.fromkeys(comps)) #not using set() to preserve order
298
+ ]
299
+ ),
300
+ bspecies,
301
+ )
302
+ ).props("inline").style("font-weight: bold; font-size: larger")
303
+ with ui.row().classes("items-center"):
304
+ ui.markdown(
305
+ r"$$" + logtxt + "{K_{" + bspecies + "}} =$$", extras=["latex"]
306
+ ).classes("mb-2 text-center").props("inline")
307
+ ui.number('logK',placeholder="Enter binding constant").classes(
308
+ "mb-2 w-20"
309
+ ).mark(f"logK-{bspecies}-val").bind_value(
310
+ self.sm.active_model.binding_constants[i], "logK"
311
+ ).props(
312
+ "inline"
313
+ ).on_value_change(lambda e="k_changed": self.sm.notify_listeners(e))
314
+ ui.label("(log-units)")
315
+ if self.mode == "fit":
316
+ with ui.row().classes("items-center"):
317
+ ui.checkbox("Vary", value=True).bind_value(
318
+ self.sm.active_model.binding_constants[i], "vary"
319
+ )
320
+ min_input = ui.number(
321
+ "min", placeholder="Minimum value", value=2
322
+ ).bind_value(self.sm.active_model.binding_constants[i], "min").classes("w-15")
323
+ max_input = ui.number(
324
+ "max", placeholder="Maxmimum value", value=20
325
+ ).bind_value(self.sm.active_model.binding_constants[i], "max").classes("w-15")
326
+
327
+ # Bind enabled state to the vary checkbox
328
+ min_input.bind_enabled_from(self.sm.active_model.binding_constants[i], "vary")
329
+ max_input.bind_enabled_from(self.sm.active_model.binding_constants[i], "vary")
330
+
@@ -0,0 +1,276 @@
1
+ from .base import BaseComponent
2
+ from nicegui import ui
3
+ import numpy as np
4
+ import pandas as pd
5
+ from . import (
6
+ BayesPanel,
7
+ BindingModelPanel,
8
+ BindToolsHeader,
9
+ DataGenerationPanel,
10
+ DataImportPanel,
11
+ DataModelPanel,
12
+ FittingPanel,
13
+ SimulationPanel,
14
+ )
15
+ from typing import Any
16
+
17
+ TabKey = tuple[str, ...]
18
+
19
+
20
+ def _append_reason(reason_map: dict[TabKey, list[str]], tab_key: TabKey, reason: str) -> None:
21
+ if tab_key not in reason_map:
22
+ reason_map[tab_key] = []
23
+ if reason not in reason_map[tab_key]:
24
+ reason_map[tab_key].append(reason)
25
+
26
+
27
+ def _compute_tab_disable_reasons(sm) -> dict[TabKey, list[str]]:
28
+ """Return deterministic disable reasons for each tab key."""
29
+ reasons: dict[TabKey, list[str]] = {}
30
+
31
+ active_model = getattr(sm, "active_model", None)
32
+ model_is_valid = True
33
+ if active_model is None:
34
+ model_is_valid = False
35
+ else:
36
+ eq_str = getattr(active_model, "eq_str", None)
37
+ binding_constants = getattr(active_model, "binding_constants", []) or []
38
+ missing_logk = any(getattr(k, "logK", None) is None for k in binding_constants)
39
+ model_is_valid = bool(eq_str is not None and str(eq_str).strip()) and not missing_logk
40
+
41
+ if not model_is_valid:
42
+ msg = "Model is incomplete. Define/parse equations and set all binding constants."
43
+ _append_reason(reasons, ("Simulate", "Data Generation"), msg)
44
+ _append_reason(reasons, ("Simulate", "Simulation"), msg)
45
+ _append_reason(reasons, ("Fit", "Data model"), msg)
46
+ _append_reason(reasons, ("Fit", "MCMC"), msg)
47
+
48
+ comp_concs = getattr(active_model, "component_concs", None) if active_model is not None else None
49
+ has_comp_concs = isinstance(comp_concs, pd.DataFrame) and not comp_concs.empty
50
+ if not has_comp_concs:
51
+ _append_reason(
52
+ reasons,
53
+ ("Simulate", "Simulation"),
54
+ "Generate component concentrations first (Simulate > Data Generation).",
55
+ )
56
+
57
+ active_expt = getattr(sm, "active_expt_data_or_none", None)
58
+ has_expt_data = (
59
+ active_expt is not None
60
+ and getattr(active_expt, "data", None) is not None
61
+ and not active_expt.data.empty
62
+ )
63
+ if not has_expt_data:
64
+ msg = "Import/select experimental data first (Fit > Import data)."
65
+ _append_reason(reasons, ("Fit", "Fit results"), msg)
66
+ _append_reason(reasons, ("Fit", "Data model"), msg)
67
+ _append_reason(reasons, ("Fit", "MCMC"), msg)
68
+
69
+ has_raw_for_expt = False
70
+ if has_expt_data:
71
+ raw_obj = getattr(active_expt, "raw_data", None)
72
+ raw_df = getattr(raw_obj, "data", None) if raw_obj is not None else None
73
+ has_raw_for_expt = raw_df is not None and not raw_df.empty
74
+ if not has_raw_for_expt:
75
+ _append_reason(
76
+ reasons,
77
+ ("Fit", "Data model"),
78
+ "Active dataset has no raw data backing it.",
79
+ )
80
+
81
+ if has_expt_data:
82
+ integ_to_spec = getattr(active_expt, "integ_to_spec", None)
83
+ limiting_shifts = getattr(active_expt, "limiting_shifts", None)
84
+ is_analytical_fast_ex = bool(getattr(active_expt, "is_analytical_fast_ex", False))
85
+ has_linear_obs = False
86
+ has_linear_obs_fn = getattr(active_expt, "has_linear_obs", None)
87
+ if callable(has_linear_obs_fn):
88
+ expt_dtypes = getattr(sm, "_expt_dtypes", {})
89
+ has_linear_obs = bool(has_linear_obs_fn(expt_dtypes))
90
+ has_integ_mapping = isinstance(integ_to_spec, np.ndarray) and integ_to_spec.size > 0
91
+ has_shift_mapping = isinstance(limiting_shifts, dict) and len(limiting_shifts) > 0
92
+ has_data_model = has_integ_mapping or has_shift_mapping or is_analytical_fast_ex or has_linear_obs
93
+ if not has_data_model:
94
+ msg = "Configure a data model first (Fit > Data model)."
95
+ _append_reason(reasons, ("Fit", "Fit results"), msg)
96
+ _append_reason(reasons, ("Fit", "MCMC"), msg)
97
+
98
+ if getattr(sm, "active_fit_id", None) is None:
99
+ _append_reason(reasons, ("Fit", "MCMC"), "Run a fit first (Fit > Results).")
100
+
101
+ return reasons
102
+
103
+
104
+ def _format_disable_tooltip(reasons: list[str]) -> str:
105
+ if not reasons:
106
+ return ""
107
+ if len(reasons) == 1:
108
+ return reasons[0]
109
+ return "\n\n".join([f"- {reason}" for reason in reasons])
110
+
111
+
112
+ class Body(BaseComponent):
113
+
114
+ def setup_nicegui(self):
115
+ self.tabs = {}
116
+ self.components = {}
117
+ self._tab_tooltip_elements: dict[TabKey, Any] = {}
118
+ self._tab_help_cues: dict[TabKey, Any] = {}
119
+ self._generate_body()
120
+ self.enable_disable_tabs()
121
+
122
+ def setup_bindings(self):
123
+ super().setup_bindings()
124
+
125
+ self.sm.add_listener("data_imported", self.enable_disable_tabs)
126
+ self.sm.add_listener("model_changed", self.enable_disable_tabs)
127
+ self.sm.add_listener("model_parsed", self.enable_disable_tabs)
128
+ self.sm.add_listener("comp_concs_updated", self.enable_disable_tabs)
129
+ self.sm.add_listener("k_changed", self.enable_disable_tabs)
130
+ self.sm.add_listener("data_model_processed", self.enable_disable_tabs)
131
+ self.sm.add_listener("fit_completed", self.enable_disable_tabs)
132
+ self.sm.add_listener("active_context_changed", self.enable_disable_tabs)
133
+
134
+ def _generate_body(self):
135
+ """Generate the body of the application."""
136
+ with ui.row().classes("w-full flex-grow p-4"):
137
+ with ui.tabs().classes("w-full").on(
138
+ "update:model-value", self.sm.save_to_storage
139
+ ) as tabs_main:
140
+ self.tabs[('Simulate',)]=ui.tab("Simulate", icon="insights")
141
+ self.tabs[('Fit',)]=ui.tab("Fit", icon="model_training")
142
+
143
+ with ui.tab_panels(tabs_main).classes("w-full"):
144
+ with ui.tab_panel("Simulate"):
145
+ with ui.tabs().classes("w-full").on(
146
+ "update:model-value", self.sim_tab_changed) as sim_tabs:
147
+ # self.tabs[('Simulate','Model Setup')]=ui.tab("Model Setup", icon="science")
148
+ self.tabs[('Simulate','Binding model setup')]=ui.tab(
149
+ "Binding model setup", label="Define model", icon="settings"
150
+ )
151
+ self.tabs[('Simulate','Data Generation')]=ui.tab("Data Generation", icon="add")
152
+ self.tabs[('Simulate','Simulation')]=ui.tab("Simulation", icon="insights")
153
+
154
+ with ui.tab_panels(sim_tabs).classes("w-full"):
155
+ with ui.tab_panel("Binding model setup"):
156
+ self.components["model_setup"] = BindingModelPanel(
157
+ self.sm, mode="sim"
158
+ )
159
+
160
+ with ui.tab_panel("Data Generation"):
161
+ self.components["data_generation"] = DataGenerationPanel(
162
+ self.sm
163
+ )
164
+
165
+ with ui.tab_panel("Simulation"):
166
+ self.components["simulation"] = SimulationPanel(
167
+ self.sm
168
+ )
169
+
170
+ with ui.tab_panel("Fit"):
171
+ with ui.tabs().classes("w-full").on(
172
+ "update:model-value", self.fit_tab_changed
173
+ ) as fit_tabs:
174
+ self.tabs[('Fit','Binding model setup')]=ui.tab(
175
+ "Binding model setup", label="Define model", icon="settings"
176
+ )
177
+ self.tabs[('Fit','Data import')]=ui.tab("Data import", label="Import data", icon="file_upload")
178
+ self.tabs[('Fit','Data model')]=ui.tab(
179
+ "Data model setup", label="Data model", icon="data_usage"
180
+ )
181
+ self.tabs[('Fit','Fit results')]=ui.tab("Fit results", label="Results", icon="check_circle")
182
+ self.tabs[('Fit','MCMC')]=ui.tab("MCMC", label="MCMC analysis", icon="calculate")
183
+
184
+ with ui.tab_panels(fit_tabs).classes("w-full"):
185
+ with ui.tab_panel("Binding model setup"):
186
+ self.components["fit_binding_model"] = BindingModelPanel(
187
+ self.sm, mode="fit"
188
+ )
189
+
190
+ with ui.tab_panel("Data import"):
191
+ self.components["data_import"] = DataImportPanel(
192
+ self.sm
193
+ )
194
+
195
+ with ui.tab_panel("Data model setup"):
196
+ self.components["data_model"] = DataModelPanel(
197
+ self.sm
198
+ )
199
+
200
+ with ui.tab_panel("Fit results"):
201
+ self.components["fit_results"] = FittingPanel(
202
+ self.sm
203
+ )
204
+
205
+ with ui.tab_panel("MCMC"):
206
+ self.components["mcmc"] = BayesPanel(
207
+ self.sm
208
+ )
209
+
210
+ def sim_tab_changed(self, e):
211
+ if e.args == 'Simulation':
212
+ # Ensure the simulation graph is updated when switching to the Simulation tab
213
+ self.components["simulation"].graph.update_graph()
214
+ self.sm.save_to_storage()
215
+
216
+ def fit_tab_changed(self, e):
217
+ if e.args == 'Data model setup':
218
+ # Ensure the data model is updated when switching to the Data model setup tab
219
+ self.components["data_model"]._populate_blocks()
220
+ if e.args == 'Fit Results':
221
+ pass
222
+ # Ensure the fit results graph is updated when switching to the Fit Results tab
223
+ # self.components["fit_results"].graph.update_graph_x()
224
+ # self.components["fit_results"].graph.graph.update()
225
+ self.sm.save_to_storage()
226
+
227
+ def enable_disable_tabs(self,e=None):
228
+ """Enable or disable tabs based on the current state."""
229
+ reason_map = _compute_tab_disable_reasons(self.sm)
230
+ tabs_to_disable = set(reason_map.keys())
231
+ tabs_to_enable = [x for x in self.tabs.keys() if x not in tabs_to_disable]
232
+ self.disable_tabs(reason_map)
233
+ self.enable_tabs(tabs_to_enable)
234
+
235
+ def _clear_tab_guidance(self, tab_key: TabKey) -> None:
236
+ tooltip = self._tab_tooltip_elements.pop(tab_key, None)
237
+ cue = self._tab_help_cues.pop(tab_key, None)
238
+ if tooltip is not None:
239
+ tooltip.delete()
240
+ if cue is not None:
241
+ cue.delete()
242
+
243
+ def disable_tabs(self, tab_reasons: dict[TabKey, list[str]]) -> None:
244
+ for k, reason_list in tab_reasons.items():
245
+ if k in self.tabs:
246
+ self.tabs[k].disable()
247
+ self._clear_tab_guidance(k)
248
+ # Ensure we can absolutely-position the help cue over the tab icon.
249
+ self.tabs[k].classes("relative overflow-visible")
250
+ with self.tabs[k]:
251
+ cue = (
252
+ ui.icon("help_outline")
253
+ .classes("text-black-7")
254
+ .style(
255
+ "position:absolute;"
256
+ "left:calc(50% + 15px);"
257
+ "top:15px;"
258
+ "transform:translate(-50%, -50%);"
259
+ "font-size:12px;"
260
+ "z-index:2;"
261
+ "pointer-events:none;"
262
+ )
263
+ )
264
+ tooltip = ui.tooltip(_format_disable_tooltip(reason_list)).style('white-space: pre-wrap')
265
+ self._tab_help_cues[k] = cue
266
+ self._tab_tooltip_elements[k] = tooltip
267
+ else:
268
+ print(f"Tab {k} not found in tabs dictionary.")
269
+
270
+ def enable_tabs(self, tab_keys: list[TabKey]) -> None:
271
+ for k in tab_keys:
272
+ if k in self.tabs:
273
+ self.tabs[k].enable()
274
+ self._clear_tab_guidance(k)
275
+ else:
276
+ print(f"Tab {k} not found in tabs dictionary.")