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,351 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from functools import partial
|
|
4
|
+
from typing import Any
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from nicegui import run, ui
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from .bayes import BayesPanel
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _classify_param_type(label: str) -> str:
|
|
14
|
+
"""Classify a prior parameter by type based on its label string."""
|
|
15
|
+
if label.startswith('log'):
|
|
16
|
+
return 'bindingConstant'
|
|
17
|
+
if label.startswith('delta0_'):
|
|
18
|
+
return 'delta'
|
|
19
|
+
if label.startswith('deltac') and '_' in label[6:]:
|
|
20
|
+
return 'deltadelta'
|
|
21
|
+
if label.startswith('shift_'):
|
|
22
|
+
return 'delta'
|
|
23
|
+
if label.startswith('eps_'):
|
|
24
|
+
return 'extinction'
|
|
25
|
+
if label.startswith('fluor_'):
|
|
26
|
+
return 'fluorAmp'
|
|
27
|
+
return 'sigma'
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
_PARAM_TYPE_CLASSES: dict[str, str] = {
|
|
31
|
+
'bindingConstant': 'bg-blue-100 text-blue-700',
|
|
32
|
+
'delta': 'bg-green-100 text-green-700',
|
|
33
|
+
'deltadelta': 'bg-teal-100 text-teal-700',
|
|
34
|
+
'sigma': 'bg-orange-100 text-orange-700',
|
|
35
|
+
'extinction': 'bg-purple-100 text-purple-700',
|
|
36
|
+
'fluorAmp': 'bg-amber-100 text-amber-700',
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class BayesPriorEditor:
|
|
41
|
+
"""Manage editable Bayesian priors for the Bayes panel."""
|
|
42
|
+
|
|
43
|
+
def __init__(self, panel: 'BayesPanel') -> None:
|
|
44
|
+
self.panel = panel
|
|
45
|
+
self.priors: list[dict[str, Any]] = []
|
|
46
|
+
|
|
47
|
+
def setup_bindings(self) -> None:
|
|
48
|
+
active_mcmc = self.panel.sm.active_mcmc_or_none
|
|
49
|
+
if active_mcmc is not None and getattr(active_mcmc, "priors", None):
|
|
50
|
+
self.priors = list(active_mcmc.priors)
|
|
51
|
+
|
|
52
|
+
async def _ensure_active_fit_model(self) -> bool:
|
|
53
|
+
"""Ensure an active fit has a bindtools model available for prior editing."""
|
|
54
|
+
active_fit = self.panel.sm.active_fit_or_none
|
|
55
|
+
active_expt_data = self.panel.sm.active_expt_data_or_none
|
|
56
|
+
if active_fit is None or active_expt_data is None:
|
|
57
|
+
return False
|
|
58
|
+
if active_fit.bd_model is not None:
|
|
59
|
+
return True
|
|
60
|
+
|
|
61
|
+
ui.notify('Preparing fit model for prior editor. This may take a second...', type='info')
|
|
62
|
+
try:
|
|
63
|
+
model = self.panel.sm.generate_binding_model_for_fit(active_fit)
|
|
64
|
+
skip_col = int(active_expt_data.col_to_comp.shape[0])
|
|
65
|
+
model = await run.cpu_bound(
|
|
66
|
+
partial(
|
|
67
|
+
model.runModel,
|
|
68
|
+
ret=True,
|
|
69
|
+
skip_col=skip_col,
|
|
70
|
+
method=active_fit.fit_method,
|
|
71
|
+
)
|
|
72
|
+
)
|
|
73
|
+
active_fit.bd_model = model
|
|
74
|
+
self.panel.sm.save_to_storage()
|
|
75
|
+
ui.notify('Fit model is ready. You can now edit priors.', type='positive')
|
|
76
|
+
return True
|
|
77
|
+
except Exception as e:
|
|
78
|
+
ui.notify(f'Unable to prepare fit model for priors: {e}', type='negative')
|
|
79
|
+
return False
|
|
80
|
+
|
|
81
|
+
def _current_prior_specs(self) -> list[dict[str, Any]]:
|
|
82
|
+
active_fit = self.panel.sm.active_fit_or_none
|
|
83
|
+
active_expt_data = self.panel.sm.active_expt_data_or_none
|
|
84
|
+
if active_fit is None or active_fit.bd_model is None or active_expt_data is None:
|
|
85
|
+
return []
|
|
86
|
+
|
|
87
|
+
model = active_fit.bd_model
|
|
88
|
+
mini_result_params = getattr(model.miniResult, "params", None)
|
|
89
|
+
if mini_result_params is None:
|
|
90
|
+
return []
|
|
91
|
+
|
|
92
|
+
specs: list[dict[str, Any]] = []
|
|
93
|
+
for param_name in mini_result_params.keys():
|
|
94
|
+
if not mini_result_params[param_name].vary:
|
|
95
|
+
continue
|
|
96
|
+
param = model.params[param_name]
|
|
97
|
+
label_str = str(getattr(param, 'name', param_name))
|
|
98
|
+
specs.append({
|
|
99
|
+
'label': label_str,
|
|
100
|
+
'lower': float(param.min),
|
|
101
|
+
'upper': float(param.max),
|
|
102
|
+
'fit_value': float(mini_result_params[param_name].value),
|
|
103
|
+
'param_type': _classify_param_type(label_str),
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
seen_sigma_names: set[str] = set()
|
|
107
|
+
for obs in active_expt_data.get_obs_list(self.panel.sm._expt_dtypes):
|
|
108
|
+
if obs.name in seen_sigma_names:
|
|
109
|
+
continue
|
|
110
|
+
seen_sigma_names.add(obs.name)
|
|
111
|
+
sigma_param = obs.param
|
|
112
|
+
sigma_fit_value = None
|
|
113
|
+
try:
|
|
114
|
+
sigma_fit_value = None if sigma_param.value is None else float(sigma_param.value)
|
|
115
|
+
except (TypeError, ValueError):
|
|
116
|
+
sigma_fit_value = None
|
|
117
|
+
specs.append({
|
|
118
|
+
'label': str(getattr(sigma_param, 'name', obs.name)),
|
|
119
|
+
'lower': float(sigma_param.min),
|
|
120
|
+
'upper': float(sigma_param.max),
|
|
121
|
+
'fit_value': sigma_fit_value,
|
|
122
|
+
'param_type': 'sigma',
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
return specs
|
|
126
|
+
|
|
127
|
+
def _default_prior_rows(self) -> list[dict[str, Any]]:
|
|
128
|
+
return [
|
|
129
|
+
{
|
|
130
|
+
'label': spec['label'],
|
|
131
|
+
'type': 'uniform',
|
|
132
|
+
'params': {'lower': spec['lower'], 'upper': spec['upper']},
|
|
133
|
+
}
|
|
134
|
+
for spec in self._current_prior_specs()
|
|
135
|
+
]
|
|
136
|
+
|
|
137
|
+
def _sync_prior_row_controls(self, row_state: dict[str, Any]) -> None:
|
|
138
|
+
is_uniform = row_state['type_select'].value == 'Uniform'
|
|
139
|
+
row_state['lower_input'].set_enabled(is_uniform)
|
|
140
|
+
row_state['upper_input'].set_enabled(is_uniform)
|
|
141
|
+
|
|
142
|
+
async def open(self) -> None:
|
|
143
|
+
if not await self._ensure_active_fit_model():
|
|
144
|
+
ui.notify('No active fit is available to edit priors.', type='warning')
|
|
145
|
+
return
|
|
146
|
+
|
|
147
|
+
specs = self._current_prior_specs()
|
|
148
|
+
if not specs:
|
|
149
|
+
ui.notify('No active fit is available to edit priors.', type='warning')
|
|
150
|
+
return
|
|
151
|
+
|
|
152
|
+
default_rows = self._default_prior_rows()
|
|
153
|
+
if self.priors:
|
|
154
|
+
prior_by_label = {
|
|
155
|
+
str(prior.get('label', '')): prior
|
|
156
|
+
for prior in self.priors
|
|
157
|
+
if isinstance(prior, dict)
|
|
158
|
+
}
|
|
159
|
+
rows = [prior_by_label.get(spec['label'], default_row) for spec, default_row in zip(specs, default_rows)]
|
|
160
|
+
else:
|
|
161
|
+
rows = default_rows
|
|
162
|
+
row_states: list[dict[str, Any]] = []
|
|
163
|
+
|
|
164
|
+
with ui.dialog() as dialog, ui.card().classes('w-[min(980px,96vw)] max-h-[88vh] overflow-hidden bayes-priors-card'):
|
|
165
|
+
ui.label('Edit MCMC Priors').classes('text-lg font-bold')
|
|
166
|
+
ui.label('Uniform priors use lower/upper bounds. None keeps the current model bounds.').classes('text-sm text-gray-600 mb-2')
|
|
167
|
+
status_label = ui.label('').classes('text-negative mb-2')
|
|
168
|
+
with ui.scroll_area().classes('w-full').style('max-height: 65vh;'):
|
|
169
|
+
for index, spec in enumerate(specs):
|
|
170
|
+
prior = rows[index] if index < len(rows) else None
|
|
171
|
+
prior_params = prior.get('params', {}) if isinstance(prior, dict) else {}
|
|
172
|
+
if not isinstance(prior_params, dict):
|
|
173
|
+
prior_params = {}
|
|
174
|
+
with ui.card().classes('w-full mb-2'):
|
|
175
|
+
with ui.row().classes('w-full items-center gap-3 flex-wrap'):
|
|
176
|
+
ui.label(spec['label']).classes('font-semibold w-60')
|
|
177
|
+
param_type = spec['param_type']
|
|
178
|
+
ui.label(param_type).classes(
|
|
179
|
+
'text-xs font-medium px-2 py-0.5 rounded '
|
|
180
|
+
+ _PARAM_TYPE_CLASSES.get(param_type, 'bg-gray-100 text-gray-700')
|
|
181
|
+
)
|
|
182
|
+
type_select = ui.select(
|
|
183
|
+
options=['Uniform', 'None'],
|
|
184
|
+
value='Uniform' if prior is None else ('None' if str(prior.get('type', 'uniform')).lower() == 'none' else 'Uniform'),
|
|
185
|
+
label='Prior type',
|
|
186
|
+
).classes('w-40')
|
|
187
|
+
lower_input = ui.number(
|
|
188
|
+
label='Lower',
|
|
189
|
+
value=spec['lower'] if prior is None else prior_params.get('lower', spec['lower']),
|
|
190
|
+
).classes('w-32')
|
|
191
|
+
upper_input = ui.number(
|
|
192
|
+
label='Upper',
|
|
193
|
+
value=spec['upper'] if prior is None else prior_params.get('upper', spec['upper']),
|
|
194
|
+
).classes('w-32')
|
|
195
|
+
fit_val = spec.get('fit_value')
|
|
196
|
+
fit_text = 'N/A' if fit_val is None else f'{float(fit_val):.6g}'
|
|
197
|
+
ui.label(f'Current fitted: {fit_text}').classes('text-xs text-gray-600')
|
|
198
|
+
row_state = {
|
|
199
|
+
'label': spec['label'],
|
|
200
|
+
'type_select': type_select,
|
|
201
|
+
'lower_input': lower_input,
|
|
202
|
+
'upper_input': upper_input,
|
|
203
|
+
'fit_value': fit_val,
|
|
204
|
+
'param_type': param_type,
|
|
205
|
+
}
|
|
206
|
+
row_states.append(row_state)
|
|
207
|
+
|
|
208
|
+
def _on_type_change(_=None, row_state=row_state):
|
|
209
|
+
self._sync_prior_row_controls(row_state)
|
|
210
|
+
|
|
211
|
+
type_select.on_value_change(_on_type_change)
|
|
212
|
+
self._sync_prior_row_controls(row_state)
|
|
213
|
+
|
|
214
|
+
async def _open_apply_modal(source_row=row_state) -> None:
|
|
215
|
+
pt = source_row['param_type']
|
|
216
|
+
same_type_others = [r for r in row_states if r is not source_row and r['param_type'] == pt]
|
|
217
|
+
if not same_type_others:
|
|
218
|
+
ui.notify(f'No other {pt} parameters to apply to.', type='info')
|
|
219
|
+
return
|
|
220
|
+
|
|
221
|
+
with ui.dialog() as apply_dialog, ui.card().classes('w-[min(520px,92vw)]'):
|
|
222
|
+
ui.label(f'Apply to all {pt} parameters').classes('text-lg font-bold mb-1')
|
|
223
|
+
ui.label(
|
|
224
|
+
'Choose how to apply the current bounds to the other '
|
|
225
|
+
f'{len(same_type_others)} {pt} parameter(s):'
|
|
226
|
+
).classes('text-sm text-gray-600 mb-3')
|
|
227
|
+
with ui.column().classes('w-full gap-2'):
|
|
228
|
+
ui.button(
|
|
229
|
+
'Copy these bounds exactly',
|
|
230
|
+
on_click=lambda: apply_dialog.submit('copy'),
|
|
231
|
+
).classes('w-full')
|
|
232
|
+
ui.button(
|
|
233
|
+
"Shift to each parameter's fitted value (keep same margins)",
|
|
234
|
+
on_click=lambda: apply_dialog.submit('shift'),
|
|
235
|
+
).classes('w-full')
|
|
236
|
+
ui.button(
|
|
237
|
+
'Cancel',
|
|
238
|
+
on_click=lambda: apply_dialog.submit('cancel'),
|
|
239
|
+
).classes('w-full')
|
|
240
|
+
|
|
241
|
+
mode = await apply_dialog
|
|
242
|
+
if mode == 'cancel' or mode is None:
|
|
243
|
+
return
|
|
244
|
+
|
|
245
|
+
src_lower = source_row['lower_input'].value
|
|
246
|
+
src_upper = source_row['upper_input'].value
|
|
247
|
+
src_type = source_row['type_select'].value
|
|
248
|
+
src_fit = source_row.get('fit_value')
|
|
249
|
+
|
|
250
|
+
for other in same_type_others:
|
|
251
|
+
if (
|
|
252
|
+
mode == 'shift'
|
|
253
|
+
and src_type == 'Uniform'
|
|
254
|
+
and src_lower is not None
|
|
255
|
+
and src_upper is not None
|
|
256
|
+
and src_fit is not None
|
|
257
|
+
):
|
|
258
|
+
tgt_fit = other.get('fit_value')
|
|
259
|
+
if tgt_fit is not None:
|
|
260
|
+
margin_below = float(src_fit) - float(src_lower)
|
|
261
|
+
margin_above = float(src_upper) - float(src_fit)
|
|
262
|
+
other['lower_input'].value = float(tgt_fit) - margin_below
|
|
263
|
+
other['upper_input'].value = float(tgt_fit) + margin_above
|
|
264
|
+
else:
|
|
265
|
+
other['lower_input'].value = src_lower
|
|
266
|
+
other['upper_input'].value = src_upper
|
|
267
|
+
else:
|
|
268
|
+
other['lower_input'].value = src_lower
|
|
269
|
+
other['upper_input'].value = src_upper
|
|
270
|
+
other['type_select'].value = src_type
|
|
271
|
+
self._sync_prior_row_controls(other)
|
|
272
|
+
|
|
273
|
+
ui.button(
|
|
274
|
+
f'Apply to all ({param_type})',
|
|
275
|
+
on_click=_open_apply_modal,
|
|
276
|
+
).classes('text-xs')
|
|
277
|
+
|
|
278
|
+
async def _save_priors() -> None:
|
|
279
|
+
saved_rows: list[dict[str, Any]] = []
|
|
280
|
+
out_of_range_rows: list[str] = []
|
|
281
|
+
for row_state in row_states:
|
|
282
|
+
prior_type = row_state['type_select'].value
|
|
283
|
+
if prior_type == 'None':
|
|
284
|
+
saved_rows.append({
|
|
285
|
+
'label': row_state['label'],
|
|
286
|
+
'type': 'none',
|
|
287
|
+
'params': {},
|
|
288
|
+
})
|
|
289
|
+
continue
|
|
290
|
+
|
|
291
|
+
lower_value = row_state['lower_input'].value
|
|
292
|
+
upper_value = row_state['upper_input'].value
|
|
293
|
+
if lower_value is None or upper_value is None:
|
|
294
|
+
status_label.text = f"{row_state['label']}: lower and upper bounds are required for a uniform prior."
|
|
295
|
+
return
|
|
296
|
+
try:
|
|
297
|
+
lower_float = float(lower_value)
|
|
298
|
+
upper_float = float(upper_value)
|
|
299
|
+
except (TypeError, ValueError):
|
|
300
|
+
status_label.text = f"{row_state['label']}: bounds must be numeric."
|
|
301
|
+
return
|
|
302
|
+
if lower_float > upper_float:
|
|
303
|
+
status_label.text = f"{row_state['label']}: lower bound must be less than or equal to upper bound."
|
|
304
|
+
return
|
|
305
|
+
|
|
306
|
+
saved_rows.append({
|
|
307
|
+
'label': row_state['label'],
|
|
308
|
+
'type': 'uniform',
|
|
309
|
+
'params': {'lower': lower_float, 'upper': upper_float},
|
|
310
|
+
})
|
|
311
|
+
fit_value = row_state.get('fit_value')
|
|
312
|
+
if fit_value is not None and not (lower_float <= float(fit_value) <= upper_float):
|
|
313
|
+
out_of_range_rows.append(
|
|
314
|
+
f"{row_state['label']} (fit={float(fit_value):.6g}, bounds=[{lower_float:.6g}, {upper_float:.6g}])"
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
if out_of_range_rows:
|
|
318
|
+
with ui.dialog() as confirm_dialog, ui.card().classes('w-[min(760px,92vw)]'):
|
|
319
|
+
ui.label('Bounds exclude fitted value').classes('text-lg font-bold')
|
|
320
|
+
ui.label('The current fitted value is outside the bounds for:').classes('mb-1')
|
|
321
|
+
ui.markdown('\n'.join([f'- {item}' for item in out_of_range_rows]))
|
|
322
|
+
ui.label('Are you sure you want to save these priors?').classes('mt-1')
|
|
323
|
+
with ui.row().classes('w-full justify-end gap-2 mt-2'):
|
|
324
|
+
ui.button('Cancel', on_click=lambda: confirm_dialog.submit(False))
|
|
325
|
+
ui.button('Save anyway', on_click=lambda: confirm_dialog.submit(True))
|
|
326
|
+
should_continue = await confirm_dialog
|
|
327
|
+
confirm_dialog.delete()
|
|
328
|
+
if not should_continue:
|
|
329
|
+
return
|
|
330
|
+
|
|
331
|
+
self.priors = saved_rows
|
|
332
|
+
if hasattr(self.panel, 'mcmc'):
|
|
333
|
+
self.panel.mcmc.priors = list(saved_rows)
|
|
334
|
+
self.panel.sm.save_to_storage()
|
|
335
|
+
dialog.submit(True)
|
|
336
|
+
|
|
337
|
+
with ui.row().classes('w-full justify-end gap-2 mt-2'):
|
|
338
|
+
ui.button('Cancel', on_click=lambda: dialog.submit(False))
|
|
339
|
+
ui.button('Save priors', on_click=_save_priors)
|
|
340
|
+
|
|
341
|
+
dialog.open()
|
|
342
|
+
await ui.run_javascript(
|
|
343
|
+
'(function attempt(n) {'
|
|
344
|
+
' var els = document.querySelectorAll(".bayes-priors-card input[type=number]");'
|
|
345
|
+
' els.forEach(function(el) {'
|
|
346
|
+
' el.addEventListener("wheel", function(e) { e.preventDefault(); }, {passive: false});'
|
|
347
|
+
' });'
|
|
348
|
+
' if (els.length === 0 && n > 0) { setTimeout(function() { attempt(n - 1); }, 50); }'
|
|
349
|
+
'})(10);'
|
|
350
|
+
)
|
|
351
|
+
await dialog
|