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,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.")
|