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,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