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,727 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Export BindTools project state to Jupyter notebooks.
|
|
3
|
+
|
|
4
|
+
Public API
|
|
5
|
+
----------
|
|
6
|
+
export_simulation_notebook(sim, model) -> dict
|
|
7
|
+
Returns an nbformat-4 notebook dict ready to be written as .ipynb JSON.
|
|
8
|
+
|
|
9
|
+
export_fit_notebook(fit, model, expt_data, raw_data) -> tuple[dict, pd.DataFrame]
|
|
10
|
+
Returns (notebook_dict, csv_dataframe).
|
|
11
|
+
Write the DataFrame to a CSV file alongside the notebook so the generated
|
|
12
|
+
code can load it with ``pd.read_csv("data.csv")``.
|
|
13
|
+
|
|
14
|
+
Both functions produce self-contained notebooks that depend only on standard
|
|
15
|
+
scientific Python libraries (numpy, pandas, matplotlib, lmfit) and
|
|
16
|
+
``bindtools``; they never import ``nicegui`` or anything from ``webgui``.
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from datetime import datetime
|
|
21
|
+
from typing import TYPE_CHECKING
|
|
22
|
+
|
|
23
|
+
import numpy as np
|
|
24
|
+
import pandas as pd
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING: # avoid runtime webgui imports
|
|
27
|
+
from ..classes.ExptData import ExptData
|
|
28
|
+
from ..classes.FitResult import FitResult
|
|
29
|
+
from ..classes.MCMCSim import MCMCSim
|
|
30
|
+
from ..classes.Model import Model
|
|
31
|
+
from ..classes.RawData import RawData
|
|
32
|
+
from ..classes.Simulation import Simulation
|
|
33
|
+
|
|
34
|
+
from ..utils import safe_filename
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# ---------------------------------------------------------------------------
|
|
38
|
+
# Low-level notebook construction helpers
|
|
39
|
+
# ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
def _md_cell(source: str) -> dict:
|
|
42
|
+
return {"cell_type": "markdown", "metadata": {}, "source": source}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _code_cell(source: str) -> dict:
|
|
46
|
+
return {
|
|
47
|
+
"cell_type": "code",
|
|
48
|
+
"execution_count": None,
|
|
49
|
+
"metadata": {},
|
|
50
|
+
"outputs": [],
|
|
51
|
+
"source": source,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _notebook(cells: list[dict]) -> dict:
|
|
56
|
+
return {
|
|
57
|
+
"nbformat": 4,
|
|
58
|
+
"nbformat_minor": 5,
|
|
59
|
+
"metadata": {
|
|
60
|
+
"kernelspec": {
|
|
61
|
+
"display_name": "Python 3",
|
|
62
|
+
"language": "python",
|
|
63
|
+
"name": "python3",
|
|
64
|
+
},
|
|
65
|
+
"language_info": {"name": "python", "version": "3.12.0"},
|
|
66
|
+
},
|
|
67
|
+
"cells": cells,
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# ---------------------------------------------------------------------------
|
|
72
|
+
# Simulation notebook
|
|
73
|
+
# ---------------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
def export_simulation_notebook(sim: "Simulation", model: "Model") -> dict:
|
|
76
|
+
"""Export a Simulation as an nbformat-4 notebook dict.
|
|
77
|
+
|
|
78
|
+
The generated notebook:
|
|
79
|
+
1. Embeds component concentrations from *sim.comp_concs* as inline data.
|
|
80
|
+
2. Uses binding constants from *model.binding_constants*.
|
|
81
|
+
3. Calls ``bd.bindingModel(...).calcSpeciation()`` to reproduce the result.
|
|
82
|
+
"""
|
|
83
|
+
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M")
|
|
84
|
+
|
|
85
|
+
# ------------------------------------------------------------------
|
|
86
|
+
# Cell 0 — Title / metadata (markdown)
|
|
87
|
+
# ------------------------------------------------------------------
|
|
88
|
+
md_title = "\n".join([
|
|
89
|
+
f"# Simulation: {sim.name or 'Unnamed'}",
|
|
90
|
+
"",
|
|
91
|
+
f"**Model:** {model.name} ",
|
|
92
|
+
f"**Equation:** `{model.eq_str}` ",
|
|
93
|
+
f"**Generated:** {timestamp} ",
|
|
94
|
+
"",
|
|
95
|
+
"| Parameter | logK |",
|
|
96
|
+
"|-----------|------|",
|
|
97
|
+
*[
|
|
98
|
+
f"| log{bc.species} | {bc.logK if bc.logK is not None else 0.0} |"
|
|
99
|
+
for bc in model.binding_constants
|
|
100
|
+
],
|
|
101
|
+
])
|
|
102
|
+
|
|
103
|
+
# ------------------------------------------------------------------
|
|
104
|
+
# Cell 1 — Imports
|
|
105
|
+
# ------------------------------------------------------------------
|
|
106
|
+
imports = (
|
|
107
|
+
"import numpy as np\n"
|
|
108
|
+
"import pandas as pd\n"
|
|
109
|
+
"import matplotlib.pyplot as plt\n"
|
|
110
|
+
"import bindtools.binding as bd"
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# ------------------------------------------------------------------
|
|
114
|
+
# Cell 2 — Model definition
|
|
115
|
+
# ------------------------------------------------------------------
|
|
116
|
+
eq_mat_list = model.eq_mat.tolist()
|
|
117
|
+
model_def = "\n".join([
|
|
118
|
+
"# Model definition",
|
|
119
|
+
f"eq_mat = np.array({eq_mat_list!r}, dtype=float)",
|
|
120
|
+
f"component_names = {model.component_names!r}",
|
|
121
|
+
f"species_names = {model.species!r}",
|
|
122
|
+
])
|
|
123
|
+
|
|
124
|
+
# ------------------------------------------------------------------
|
|
125
|
+
# Cell 3 — Component concentrations (inlined from sim.comp_concs)
|
|
126
|
+
# ------------------------------------------------------------------
|
|
127
|
+
comp_dict = {col: sim.comp_concs[col].tolist() for col in sim.comp_concs.columns}
|
|
128
|
+
comp_concs_code = "\n".join([
|
|
129
|
+
"# Component concentrations",
|
|
130
|
+
f"comp_concs = pd.DataFrame({comp_dict!r})",
|
|
131
|
+
])
|
|
132
|
+
|
|
133
|
+
# ------------------------------------------------------------------
|
|
134
|
+
# Cell 4 — Binding parameters
|
|
135
|
+
# ------------------------------------------------------------------
|
|
136
|
+
param_lines = [
|
|
137
|
+
"# Binding parameters (log10 association constants)",
|
|
138
|
+
"params = {",
|
|
139
|
+
]
|
|
140
|
+
for bc in model.binding_constants:
|
|
141
|
+
val = bc.logK if bc.logK is not None else 0.0
|
|
142
|
+
comment = " # component pseudo-constant" if bc.isComp else ""
|
|
143
|
+
param_lines.append(f" 'log{bc.species}': {val!r},{comment}")
|
|
144
|
+
param_lines.append("}")
|
|
145
|
+
params_code = "\n".join(param_lines)
|
|
146
|
+
|
|
147
|
+
# ------------------------------------------------------------------
|
|
148
|
+
# Cell 5 — Run the binding model and compute speciation
|
|
149
|
+
# ------------------------------------------------------------------
|
|
150
|
+
run_code = (
|
|
151
|
+
"# Set up and run the binding model\n"
|
|
152
|
+
"bm = bd.bindingModel(\n"
|
|
153
|
+
" eq_mat,\n"
|
|
154
|
+
" component_names,\n"
|
|
155
|
+
" species_names,\n"
|
|
156
|
+
" compConcs=comp_concs.values,\n"
|
|
157
|
+
")\n"
|
|
158
|
+
"bm.prepModel()\n"
|
|
159
|
+
"\n"
|
|
160
|
+
"# Fix all parameters to simulation values\n"
|
|
161
|
+
"for pname, pval in params.items():\n"
|
|
162
|
+
" bm.params[pname].set(value=pval, vary=False)\n"
|
|
163
|
+
"\n"
|
|
164
|
+
"# Calculate speciation\n"
|
|
165
|
+
"spec = bm.calcSpeciation()\n"
|
|
166
|
+
"results = pd.DataFrame(spec, columns=species_names, index=comp_concs.index)\n"
|
|
167
|
+
"print(results)"
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
# ------------------------------------------------------------------
|
|
171
|
+
# Cell 6 — Plot
|
|
172
|
+
# ------------------------------------------------------------------
|
|
173
|
+
# Note: curly braces inside the string are notebook f-string syntax,
|
|
174
|
+
# evaluated when the *notebook* runs, not at generation time.
|
|
175
|
+
plot_code = (
|
|
176
|
+
"# Plot speciation\n"
|
|
177
|
+
"fig, ax = plt.subplots(figsize=(8, 5))\n"
|
|
178
|
+
"if len(component_names) >= 2:\n"
|
|
179
|
+
" x_vals = comp_concs.iloc[:, 1].values / comp_concs.iloc[:, 0].values\n"
|
|
180
|
+
" x_label = f'[{component_names[1]}]$_{{tot}}$ / [{component_names[0]}]$_{{tot}}$'\n"
|
|
181
|
+
"else:\n"
|
|
182
|
+
" x_vals = np.arange(len(comp_concs))\n"
|
|
183
|
+
" x_label = 'Step'\n"
|
|
184
|
+
"for col in results.columns:\n"
|
|
185
|
+
" ax.plot(x_vals, results[col], label=f'[{col}]$_{{free}}$')\n"
|
|
186
|
+
"ax.set_xlabel(x_label)\n"
|
|
187
|
+
"ax.set_ylabel('Concentration (M)')\n"
|
|
188
|
+
"ax.legend()\n"
|
|
189
|
+
"plt.tight_layout()\n"
|
|
190
|
+
"plt.show()"
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
cells = [
|
|
194
|
+
_md_cell(md_title),
|
|
195
|
+
_code_cell(imports),
|
|
196
|
+
_code_cell(model_def),
|
|
197
|
+
_code_cell(comp_concs_code),
|
|
198
|
+
_code_cell(params_code),
|
|
199
|
+
_code_cell(run_code),
|
|
200
|
+
_code_cell(plot_code),
|
|
201
|
+
]
|
|
202
|
+
return _notebook(cells)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
# ---------------------------------------------------------------------------
|
|
206
|
+
# Fit notebook
|
|
207
|
+
# ---------------------------------------------------------------------------
|
|
208
|
+
|
|
209
|
+
def _build_lin_obs_cell4_lines(
|
|
210
|
+
lin_obs_col_names: list,
|
|
211
|
+
lin_obs_param_map: list,
|
|
212
|
+
) -> list[str]:
|
|
213
|
+
"""Return source lines that reconstruct spec_to_linear before m.prepModel().
|
|
214
|
+
|
|
215
|
+
lin_obs_param_map is a list-of-lists: [obs_idx][spec_idx] → dict or None.
|
|
216
|
+
Active cells: dict with keys 'name', 'min', 'max'.
|
|
217
|
+
Dark cells: None.
|
|
218
|
+
"""
|
|
219
|
+
n_lin_obs = len(lin_obs_col_names)
|
|
220
|
+
lines = [
|
|
221
|
+
"",
|
|
222
|
+
"# UV-vis / fluorescence: reconstruct specToLinear before prepModel()",
|
|
223
|
+
f"_lin_obs_col_names = {lin_obs_col_names!r}",
|
|
224
|
+
f"_lin_obs_param_map = {lin_obs_param_map!r}",
|
|
225
|
+
f"_n_lin_obs = {n_lin_obs}",
|
|
226
|
+
"spec_to_linear = np.empty((len(species_names), _n_lin_obs), dtype=object)",
|
|
227
|
+
"for _oi, _prow in enumerate(_lin_obs_param_map):",
|
|
228
|
+
" for _si, _pcell in enumerate(_prow):",
|
|
229
|
+
" if _pcell is None:",
|
|
230
|
+
" spec_to_linear[_si, _oi] = 0.0",
|
|
231
|
+
" else:",
|
|
232
|
+
" spec_to_linear[_si, _oi] = lmfit.Parameter(",
|
|
233
|
+
" _pcell['name'], value=1.0, min=_pcell['min'], max=_pcell['max'], vary=True",
|
|
234
|
+
" )",
|
|
235
|
+
"m.specToLinear = spec_to_linear",
|
|
236
|
+
]
|
|
237
|
+
return lines
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _build_analytical_lin_obs_lines(lin_obs_param_map: list) -> list[str]:
|
|
241
|
+
"""Return setup lines for the analytical path's linear_obs_param_map.
|
|
242
|
+
|
|
243
|
+
Extracts the param name (or None) from each cell so
|
|
244
|
+
fitfun_analytical_fast_exchange can call calc_analytical_linear_observables.
|
|
245
|
+
"""
|
|
246
|
+
# Serialise as [[name_or_none, ...], ...]
|
|
247
|
+
name_map = [
|
|
248
|
+
[cell["name"] if cell is not None else None for cell in row]
|
|
249
|
+
for row in lin_obs_param_map
|
|
250
|
+
]
|
|
251
|
+
return [
|
|
252
|
+
f"m.analytical_linear_obs_param_map = {name_map!r}",
|
|
253
|
+
]
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def export_fit_notebook(
|
|
257
|
+
fit: "FitResult",
|
|
258
|
+
model: "Model",
|
|
259
|
+
expt_data: "ExptData",
|
|
260
|
+
raw_data: "RawData",
|
|
261
|
+
obs_type_names: "list[str] | None",
|
|
262
|
+
lin_obs_col_names: "list[str] | None" = None,
|
|
263
|
+
lin_obs_param_map: "list[list] | None" = None,
|
|
264
|
+
) -> tuple[dict, pd.DataFrame]:
|
|
265
|
+
"""Export a FitResult as an nbformat-4 notebook dict plus a CSV DataFrame.
|
|
266
|
+
|
|
267
|
+
The notebook is designed to *re-run* the fit from scratch. Original
|
|
268
|
+
fitted values are embedded as comments so the user can verify the result.
|
|
269
|
+
The companion CSV (second return value) should be saved as ``data.csv``
|
|
270
|
+
next to the notebook.
|
|
271
|
+
"""
|
|
272
|
+
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M")
|
|
273
|
+
|
|
274
|
+
n_comp = int(np.shape(expt_data.col_to_comp)[0])
|
|
275
|
+
col_to_comp_list = np.array(expt_data.col_to_comp, dtype=float).tolist()
|
|
276
|
+
|
|
277
|
+
integ_to_spec = expt_data.integ_to_spec
|
|
278
|
+
if isinstance(integ_to_spec, np.ndarray) and integ_to_spec.ndim == 2 and integ_to_spec.size > 0:
|
|
279
|
+
integ_to_spec_list: list | None = integ_to_spec.tolist()
|
|
280
|
+
else:
|
|
281
|
+
integ_to_spec_list = None
|
|
282
|
+
|
|
283
|
+
delta_to_spec = expt_data.delta_to_spec
|
|
284
|
+
has_delta = (
|
|
285
|
+
isinstance(delta_to_spec, np.ndarray)
|
|
286
|
+
and delta_to_spec.ndim == 2
|
|
287
|
+
and delta_to_spec.size > 0
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
obs_list = obs_type_names
|
|
291
|
+
# ------------------------------------------------------------------
|
|
292
|
+
# Cell 0 — Summary markdown
|
|
293
|
+
# ------------------------------------------------------------------
|
|
294
|
+
param_rows = []
|
|
295
|
+
for pname, pinfo in (fit.params or {}).items():
|
|
296
|
+
if isinstance(pinfo, dict):
|
|
297
|
+
val = pinfo.get("value", "?")
|
|
298
|
+
stderr = pinfo.get("stderr", None)
|
|
299
|
+
vary = pinfo.get("vary", False)
|
|
300
|
+
if vary and stderr is not None:
|
|
301
|
+
param_rows.append(f"| {pname} | {val:.6g} ± {stderr:.3g} | fitted |")
|
|
302
|
+
else:
|
|
303
|
+
param_rows.append(f"| {pname} | {val:.6g} | fixed |")
|
|
304
|
+
|
|
305
|
+
aic_str = f"{fit.aic:.4g}" if fit.aic is not None else "n/a"
|
|
306
|
+
bic_str = f"{fit.bic:.4g}" if fit.bic is not None else "n/a"
|
|
307
|
+
chisqr_str = f"{fit.chisqr:.4g}" if fit.chisqr is not None else "n/a"
|
|
308
|
+
method_str = fit.fit_method or "least_squares"
|
|
309
|
+
|
|
310
|
+
md_title = "\n".join([
|
|
311
|
+
f"# Fit: {fit.name or 'Unnamed'}",
|
|
312
|
+
"",
|
|
313
|
+
f"**Model:** {model.name} ",
|
|
314
|
+
f"**Equation:** `{model.eq_str}` ",
|
|
315
|
+
f"**Generated:** {timestamp} ",
|
|
316
|
+
"",
|
|
317
|
+
"## Original fit results",
|
|
318
|
+
"",
|
|
319
|
+
f"| Metric | Value |",
|
|
320
|
+
f"|--------|-------|",
|
|
321
|
+
f"| AIC | {aic_str} |",
|
|
322
|
+
f"| BIC | {bic_str} |",
|
|
323
|
+
f"| χ² | {chisqr_str} |",
|
|
324
|
+
f"| Method | {method_str} |",
|
|
325
|
+
f"| Success | {fit.success} |",
|
|
326
|
+
"",
|
|
327
|
+
"| Parameter | Value | Status |",
|
|
328
|
+
"|-----------|-------|--------|",
|
|
329
|
+
*param_rows,
|
|
330
|
+
*(
|
|
331
|
+
[f"", f"**Termination:** {fit.termination_message}"]
|
|
332
|
+
if fit.termination_message
|
|
333
|
+
else []
|
|
334
|
+
),
|
|
335
|
+
])
|
|
336
|
+
|
|
337
|
+
# ------------------------------------------------------------------
|
|
338
|
+
# Cell 1 — Imports
|
|
339
|
+
# ------------------------------------------------------------------
|
|
340
|
+
imports = (
|
|
341
|
+
"import numpy as np\n"
|
|
342
|
+
"import pandas as pd\n"
|
|
343
|
+
"import matplotlib.pyplot as plt\n"
|
|
344
|
+
"import lmfit\n"
|
|
345
|
+
"import bindtools.binding as bd\n"
|
|
346
|
+
"from IPython.display import HTML, display"
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
# ------------------------------------------------------------------
|
|
350
|
+
# Cell 2 — Model definition
|
|
351
|
+
# ------------------------------------------------------------------
|
|
352
|
+
model_def = "\n".join([
|
|
353
|
+
"# Model definition",
|
|
354
|
+
f"eq_mat = np.array({model.eq_mat.tolist()!r}, dtype=float)",
|
|
355
|
+
f"component_names = {model.component_names!r}",
|
|
356
|
+
f"species_names = {model.species!r}",
|
|
357
|
+
f"obs_list = {obs_list!r}",
|
|
358
|
+
])
|
|
359
|
+
|
|
360
|
+
# ------------------------------------------------------------------
|
|
361
|
+
# Cell 3 — Load data
|
|
362
|
+
# ------------------------------------------------------------------
|
|
363
|
+
stem = safe_filename(fit.name or "fit")
|
|
364
|
+
# Capture the column order used by the GUI (selected + reordered via column_mapping).
|
|
365
|
+
# Embedding this lets the notebook reconstruct the same data slice from the raw CSV.
|
|
366
|
+
sorted_cols = expt_data.sorted_data.columns.tolist()
|
|
367
|
+
load_data = "\n".join([
|
|
368
|
+
"# Load experimental data — keep the companion CSV alongside this notebook.",
|
|
369
|
+
"# The column list below reproduces the selection and ordering used in BindTools.",
|
|
370
|
+
f"data = pd.read_csv('{stem}_data.csv')",
|
|
371
|
+
f"data_cols = {sorted_cols!r}",
|
|
372
|
+
"data = data[data_cols]",
|
|
373
|
+
"raw = data.to_numpy(dtype=float)",
|
|
374
|
+
])
|
|
375
|
+
|
|
376
|
+
# ------------------------------------------------------------------
|
|
377
|
+
# Cell 4 — Construct binding model (mirrors reference notebook pattern)
|
|
378
|
+
# ------------------------------------------------------------------
|
|
379
|
+
col_to_comp_repr = repr(col_to_comp_list)
|
|
380
|
+
if integ_to_spec_list is not None:
|
|
381
|
+
spec_to_integ_line = f"spec_to_integ = np.array({integ_to_spec_list!r}, dtype=float)"
|
|
382
|
+
spec_to_integ_arg = "spec_to_integ"
|
|
383
|
+
else:
|
|
384
|
+
spec_to_integ_line = "spec_to_integ = None"
|
|
385
|
+
spec_to_integ_arg = "None"
|
|
386
|
+
|
|
387
|
+
if has_delta:
|
|
388
|
+
spec_to_dd_line = f"spec_to_dd = np.array({delta_to_spec.T.tolist()!r}, dtype=object)"
|
|
389
|
+
spec_to_dd_arg = "spec_to_dd"
|
|
390
|
+
else:
|
|
391
|
+
spec_to_dd_line = "spec_to_dd = None"
|
|
392
|
+
spec_to_dd_arg = "None"
|
|
393
|
+
|
|
394
|
+
is_analytical = bool(getattr(fit, "analytical_fast_exchange", False))
|
|
395
|
+
analytical_topology = getattr(fit, "analytical_topology", None)
|
|
396
|
+
analytical_obs_columns = list(getattr(fit, "analytical_obs_columns", []))
|
|
397
|
+
analytical_obs_components = list(getattr(fit, "analytical_obs_components", []))
|
|
398
|
+
analytical_complex_indices = list(getattr(fit, "analytical_complex_indices", []))
|
|
399
|
+
|
|
400
|
+
has_lin_obs = bool(lin_obs_col_names and lin_obs_param_map)
|
|
401
|
+
|
|
402
|
+
if is_analytical:
|
|
403
|
+
analytical_setup_lines: list[str] = [
|
|
404
|
+
"",
|
|
405
|
+
"# Analytical fast-exchange backend — must be configured before prepModel()",
|
|
406
|
+
"m.analytical_fast_exchange = True",
|
|
407
|
+
f"m.analytical_topology = {analytical_topology!r}",
|
|
408
|
+
f"m.analytical_obs_columns = {analytical_obs_columns!r}",
|
|
409
|
+
f"m.analytical_obs_components = {analytical_obs_components!r}",
|
|
410
|
+
f"m.analytical_complex_indices = {analytical_complex_indices!r}",
|
|
411
|
+
]
|
|
412
|
+
if has_lin_obs:
|
|
413
|
+
analytical_setup_lines += _build_analytical_lin_obs_lines(lin_obs_param_map) # type: ignore[arg-type]
|
|
414
|
+
else:
|
|
415
|
+
analytical_setup_lines = []
|
|
416
|
+
|
|
417
|
+
lin_obs_lines = (
|
|
418
|
+
_build_lin_obs_cell4_lines(lin_obs_col_names, lin_obs_param_map) # type: ignore[arg-type]
|
|
419
|
+
if has_lin_obs
|
|
420
|
+
else []
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
construct_model = "\n".join([
|
|
424
|
+
"# Matrices mapping data columns → components and species → observables",
|
|
425
|
+
f"col_to_comp = np.array({col_to_comp_repr}, dtype=float)",
|
|
426
|
+
spec_to_integ_line,
|
|
427
|
+
spec_to_dd_line,
|
|
428
|
+
"",
|
|
429
|
+
"# Construct binding model",
|
|
430
|
+
"m = bd.bindingModel(",
|
|
431
|
+
" eq_mat,",
|
|
432
|
+
" component_names,",
|
|
433
|
+
" species_names,",
|
|
434
|
+
f" {spec_to_integ_arg},",
|
|
435
|
+
f" {spec_to_dd_arg},",
|
|
436
|
+
" col_to_comp,",
|
|
437
|
+
" obs_list,",
|
|
438
|
+
" raw,",
|
|
439
|
+
")",
|
|
440
|
+
"# Compute component concentrations from the data matrix",
|
|
441
|
+
"m.compConcs = np.dot(m.rawData[:, :m.nComp], m.colToComp[:, :m.nComp].T)",
|
|
442
|
+
*lin_obs_lines,
|
|
443
|
+
*analytical_setup_lines,
|
|
444
|
+
"m.prepModel()",
|
|
445
|
+
])
|
|
446
|
+
|
|
447
|
+
# ------------------------------------------------------------------
|
|
448
|
+
# Cell 5 — Set parameters (initial_value for re-running; fitted in comments)
|
|
449
|
+
# ------------------------------------------------------------------
|
|
450
|
+
param_set_lines = [
|
|
451
|
+
"# Set parameter bounds and initial guesses.",
|
|
452
|
+
"# Original fitted values are shown in comments — use them to verify your re-run.",
|
|
453
|
+
]
|
|
454
|
+
for pname, pinfo in (fit.params or {}).items():
|
|
455
|
+
if not isinstance(pinfo, dict):
|
|
456
|
+
continue
|
|
457
|
+
vary = pinfo.get("vary", False)
|
|
458
|
+
min_val = pinfo.get("min", 0)
|
|
459
|
+
max_val = pinfo.get("max", 14)
|
|
460
|
+
init_val = pinfo.get("initial_value", pinfo.get("value", 0.0))
|
|
461
|
+
fitted_val = pinfo.get("value", init_val)
|
|
462
|
+
stderr = pinfo.get("stderr", None)
|
|
463
|
+
|
|
464
|
+
if vary and stderr is not None:
|
|
465
|
+
comment = f" # fitted: {fitted_val:.6g} ± {stderr:.3g}"
|
|
466
|
+
elif vary:
|
|
467
|
+
comment = f" # fitted: {fitted_val:.6g}"
|
|
468
|
+
else:
|
|
469
|
+
comment = " # fixed"
|
|
470
|
+
|
|
471
|
+
param_set_lines.append(
|
|
472
|
+
f"m.params[{pname!r}].set(value={float(init_val)!r}, "
|
|
473
|
+
f"vary={vary!r}, min={float(min_val)!r}, max={float(max_val)!r})"
|
|
474
|
+
f"{comment}"
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
set_params_code = "\n".join(param_set_lines)
|
|
478
|
+
|
|
479
|
+
# ------------------------------------------------------------------
|
|
480
|
+
# Cell 6 — Run the fit
|
|
481
|
+
# ------------------------------------------------------------------
|
|
482
|
+
run_fit = "\n".join([
|
|
483
|
+
"# Run the fit",
|
|
484
|
+
f"skip_col = {n_comp} # number of component-concentration columns",
|
|
485
|
+
f"m.runModel(skip_col=skip_col, method={method_str!r})",
|
|
486
|
+
"display(HTML(m.miniResult._repr_html_()))",
|
|
487
|
+
])
|
|
488
|
+
|
|
489
|
+
# ------------------------------------------------------------------
|
|
490
|
+
# Cell 7 — Plot residuals
|
|
491
|
+
# ------------------------------------------------------------------
|
|
492
|
+
plot_resid = (
|
|
493
|
+
"# Residual plot — adjust plotMask to select which observables to show\n"
|
|
494
|
+
"n_obs = len(obs_list)\n"
|
|
495
|
+
"bd.makeFitResidPlot(\n"
|
|
496
|
+
" m,\n"
|
|
497
|
+
" xindex=1,\n"
|
|
498
|
+
" plotMask=tuple(range(n_obs)),\n"
|
|
499
|
+
" skip_end=None,\n"
|
|
500
|
+
" xvals=m.rawData[:, 1] / m.rawData[:, 0],\n"
|
|
501
|
+
" xlabel=r'[' + component_names[1] + r']$_{\\mathrm{tot}}$ / [' + component_names[0] + r']$_{\\mathrm{tot}}$',\n"
|
|
502
|
+
" ylabel='Observable',\n"
|
|
503
|
+
" labels=obs_list,\n"
|
|
504
|
+
")"
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
# ------------------------------------------------------------------
|
|
508
|
+
# Cell 8 — MCMC section (markdown header)
|
|
509
|
+
# ------------------------------------------------------------------
|
|
510
|
+
md_mcmc = "\n".join([
|
|
511
|
+
"## MCMC sampling",
|
|
512
|
+
"",
|
|
513
|
+
"Use the cell below to explore the posterior distribution of the fit parameters with `emcee`.",
|
|
514
|
+
"Uncomment and adjust as needed.",
|
|
515
|
+
])
|
|
516
|
+
|
|
517
|
+
# ------------------------------------------------------------------
|
|
518
|
+
# Cell 9 — MCMC code (commented out)
|
|
519
|
+
# ------------------------------------------------------------------
|
|
520
|
+
mcmc_code = "\n".join([
|
|
521
|
+
"# # bd.MCMC requires the fit to have been run first (m.runModel above).",
|
|
522
|
+
"# # obs_types maps each observable column to its noise model.",
|
|
523
|
+
"# # Built-in names: 'NMRInteg', 'concMeas', 'deltaH', 'deltaF'.",
|
|
524
|
+
"# # Use a custom string for anything else.",
|
|
525
|
+
"# obs_types = [bd.ObsType(name) for name in obs_list]",
|
|
526
|
+
"#",
|
|
527
|
+
"# # Number of walkers and steps — increase for production runs.",
|
|
528
|
+
"# n_walkers = 50",
|
|
529
|
+
"# n_steps = 2000",
|
|
530
|
+
"#",
|
|
531
|
+
"# mc = bd.MCMC(m, obs_types, walkers=n_walkers, samples=n_steps)",
|
|
532
|
+
"# mc.run()",
|
|
533
|
+
"#",
|
|
534
|
+
"# # Inspect autocorrelation time to check convergence",
|
|
535
|
+
"# mc.get_tau()",
|
|
536
|
+
"#",
|
|
537
|
+
"# # Walker trace plot",
|
|
538
|
+
"# mc.plot_chain()",
|
|
539
|
+
"# plt.show()",
|
|
540
|
+
"#",
|
|
541
|
+
"# # Corner plot (burn-in is estimated automatically from autocorr time)",
|
|
542
|
+
"# mc.plot_corner()",
|
|
543
|
+
"# plt.show()",
|
|
544
|
+
])
|
|
545
|
+
|
|
546
|
+
cells = [
|
|
547
|
+
_md_cell(md_title),
|
|
548
|
+
_code_cell(imports),
|
|
549
|
+
_code_cell(model_def),
|
|
550
|
+
_code_cell(load_data),
|
|
551
|
+
_code_cell(construct_model),
|
|
552
|
+
_code_cell(set_params_code),
|
|
553
|
+
_code_cell(run_fit),
|
|
554
|
+
_code_cell(plot_resid),
|
|
555
|
+
_md_cell(md_mcmc),
|
|
556
|
+
_code_cell(mcmc_code),
|
|
557
|
+
]
|
|
558
|
+
|
|
559
|
+
csv_df = raw_data.data.copy()
|
|
560
|
+
return _notebook(cells), csv_df
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
# ---------------------------------------------------------------------------
|
|
564
|
+
# MCMC notebook
|
|
565
|
+
# ---------------------------------------------------------------------------
|
|
566
|
+
|
|
567
|
+
def export_mcmc_notebook(
|
|
568
|
+
mcmc: "MCMCSim | None",
|
|
569
|
+
fit: "FitResult",
|
|
570
|
+
model: "Model",
|
|
571
|
+
expt_data: "ExptData",
|
|
572
|
+
raw_data: "RawData",
|
|
573
|
+
obs_type_names: list[str],
|
|
574
|
+
include_chains: bool,
|
|
575
|
+
lin_obs_col_names: "list[str] | None" = None,
|
|
576
|
+
lin_obs_param_map: "list[list] | None" = None,
|
|
577
|
+
) -> tuple[dict, pd.DataFrame, bytes | None]:
|
|
578
|
+
"""Export a FitResult + MCMC run as an nbformat-4 notebook dict, CSV DataFrame,
|
|
579
|
+
and optionally HDF5 bytes (chains file).
|
|
580
|
+
|
|
581
|
+
Parameters
|
|
582
|
+
----------
|
|
583
|
+
mcmc : MCMCSim or None
|
|
584
|
+
The MCMC run. When *None*, a code-only notebook is produced with
|
|
585
|
+
default walker / step counts.
|
|
586
|
+
fit, model, expt_data, raw_data : linked domain objects
|
|
587
|
+
obs_type_names : list[str]
|
|
588
|
+
Noise-model name for each observable, e.g. ``['deltaF', 'deltaF']``.
|
|
589
|
+
include_chains : bool
|
|
590
|
+
When True the notebook loads chains from a companion HDF5 file and the
|
|
591
|
+
function also returns the raw HDF5 bytes. Requires
|
|
592
|
+
``mcmc.mc.sampler`` to be non-None.
|
|
593
|
+
|
|
594
|
+
Returns
|
|
595
|
+
-------
|
|
596
|
+
notebook : dict nbformat-4 compatible dict.
|
|
597
|
+
csv_df : pd.DataFrame Raw data for the companion CSV.
|
|
598
|
+
h5_bytes : bytes or None
|
|
599
|
+
"""
|
|
600
|
+
# Build the first 8 cells (title … residual plot) from the fit notebook.
|
|
601
|
+
# The fit notebook has exactly 10 cells; we discard the two commented-out
|
|
602
|
+
# MCMC template cells at the end and replace them with live code.
|
|
603
|
+
base_nb, csv_df = export_fit_notebook(
|
|
604
|
+
fit, model, expt_data, raw_data,
|
|
605
|
+
obs_type_names=obs_type_names,
|
|
606
|
+
lin_obs_col_names=lin_obs_col_names,
|
|
607
|
+
lin_obs_param_map=lin_obs_param_map,
|
|
608
|
+
)
|
|
609
|
+
cells = base_nb["cells"][:8]
|
|
610
|
+
|
|
611
|
+
# MCMC parameters — use MCMCSim values when available, fall back to defaults
|
|
612
|
+
n_walkers = int(mcmc.nwalkers) if mcmc is not None else 50
|
|
613
|
+
n_steps = int(mcmc.nsteps_target) if mcmc is not None else 2000
|
|
614
|
+
thin = int(mcmc.thin) if mcmc is not None else 1
|
|
615
|
+
burn = int(mcmc.burn) if mcmc is not None else 200
|
|
616
|
+
|
|
617
|
+
stem = safe_filename(fit.name or "fit")
|
|
618
|
+
|
|
619
|
+
# ------------------------------------------------------------------
|
|
620
|
+
# Cell 8 — MCMC section header (markdown)
|
|
621
|
+
# ------------------------------------------------------------------
|
|
622
|
+
if include_chains:
|
|
623
|
+
md_mcmc = "\n".join([
|
|
624
|
+
"## MCMC sampling — loading saved chains",
|
|
625
|
+
"",
|
|
626
|
+
f"Chains are loaded from `{stem}_chains.hdf` (included in the zip).",
|
|
627
|
+
"Adjust `burnin` as needed.",
|
|
628
|
+
])
|
|
629
|
+
else:
|
|
630
|
+
md_mcmc = "\n".join([
|
|
631
|
+
"## MCMC sampling",
|
|
632
|
+
"",
|
|
633
|
+
"Run the cell below to explore the posterior distribution via MCMC.",
|
|
634
|
+
f"Walkers: {n_walkers}, steps: {n_steps}, thin: {thin}.",
|
|
635
|
+
])
|
|
636
|
+
|
|
637
|
+
# ------------------------------------------------------------------
|
|
638
|
+
# Cell 9 — MCMC code
|
|
639
|
+
# ------------------------------------------------------------------
|
|
640
|
+
if include_chains:
|
|
641
|
+
mcmc_code = "\n".join([
|
|
642
|
+
"import h5py",
|
|
643
|
+
"import corner as corner",
|
|
644
|
+
"",
|
|
645
|
+
f"with h5py.File('{stem}_chains.hdf', 'r') as f:",
|
|
646
|
+
" chain = f['mcmc/chain'][:] # (nsteps, nwalkers, ndim)",
|
|
647
|
+
" log_prob = f['mcmc/log_prob'][:] # (nsteps, nwalkers)",
|
|
648
|
+
"",
|
|
649
|
+
"param_labels = [p for p in m.params if m.params[p].vary]",
|
|
650
|
+
"ndim = chain.shape[2]",
|
|
651
|
+
"",
|
|
652
|
+
"# Walker trace plot",
|
|
653
|
+
"n_rows = ndim + 1",
|
|
654
|
+
"fig, axes = plt.subplots(n_rows, 1, figsize=(12, 3 + n_rows * 1.7), sharex=True)",
|
|
655
|
+
"for i, label in enumerate(param_labels):",
|
|
656
|
+
" axes[i].plot(chain[:, :, i], alpha=0.3, lw=0.5, color='k')",
|
|
657
|
+
" axes[i].set_ylabel(label)",
|
|
658
|
+
"axes[-1].plot(log_prob, alpha=0.3, lw=0.5, color='k')",
|
|
659
|
+
"axes[-1].set_ylabel('log prob')",
|
|
660
|
+
"axes[-1].set_xlabel('step')",
|
|
661
|
+
"fig.tight_layout()",
|
|
662
|
+
"plt.show()",
|
|
663
|
+
"",
|
|
664
|
+
f"# Corner plot — burnin from original run ({burn}); adjust as needed",
|
|
665
|
+
f"burnin = {burn}",
|
|
666
|
+
"flat_chain = chain[burnin:].reshape(-1, ndim)",
|
|
667
|
+
"if len(param_labels) < ndim:",
|
|
668
|
+
" param_labels += [f'sigma_{i}' for i in range(len(param_labels), ndim)]",
|
|
669
|
+
"corner.corner(flat_chain, labels=param_labels)",
|
|
670
|
+
"plt.show()",
|
|
671
|
+
])
|
|
672
|
+
else:
|
|
673
|
+
mcmc_code = "\n".join([
|
|
674
|
+
f"obs_type_names = {obs_type_names!r}",
|
|
675
|
+
"obs_types = [bd.ObsType(name) for name in obs_type_names]",
|
|
676
|
+
"",
|
|
677
|
+
f"n_walkers = {n_walkers}",
|
|
678
|
+
f"n_steps = {n_steps}",
|
|
679
|
+
f"thin = {thin}",
|
|
680
|
+
"",
|
|
681
|
+
"mc = bd.MCMC(m, obs_types, walkers=n_walkers, samples=n_steps)",
|
|
682
|
+
"mc.run(thin=thin)",
|
|
683
|
+
"",
|
|
684
|
+
"# Inspect autocorrelation time to check convergence",
|
|
685
|
+
"mc.get_tau()",
|
|
686
|
+
"",
|
|
687
|
+
"# Walker trace plot",
|
|
688
|
+
"mc.plot_chain()",
|
|
689
|
+
"plt.show()",
|
|
690
|
+
"",
|
|
691
|
+
"# Corner plot (burn-in estimated automatically from autocorr time)",
|
|
692
|
+
"mc.plot_corner()",
|
|
693
|
+
"plt.show()",
|
|
694
|
+
"",
|
|
695
|
+
f"# mc.save('{stem}_chains.hdf') # uncomment to save chains",
|
|
696
|
+
])
|
|
697
|
+
|
|
698
|
+
cells.append(_md_cell(md_mcmc))
|
|
699
|
+
cells.append(_code_cell(mcmc_code))
|
|
700
|
+
|
|
701
|
+
# ------------------------------------------------------------------
|
|
702
|
+
# HDF5 bytes (include_chains=True only)
|
|
703
|
+
# ------------------------------------------------------------------
|
|
704
|
+
h5_bytes: bytes | None = None
|
|
705
|
+
if (
|
|
706
|
+
include_chains
|
|
707
|
+
and mcmc is not None
|
|
708
|
+
and getattr(mcmc, "mc", None) is not None
|
|
709
|
+
and getattr(mcmc.mc, "sampler", None) is not None
|
|
710
|
+
):
|
|
711
|
+
import h5py # lazy import — h5py only needed at export time
|
|
712
|
+
|
|
713
|
+
backend = mcmc.mc.sampler.backend
|
|
714
|
+
with h5py.File("mcmc_export.h5", "w", driver="core", backing_store=False) as hf:
|
|
715
|
+
g = hf.create_group("mcmc")
|
|
716
|
+
g.create_dataset("chain", data=backend.chain)
|
|
717
|
+
g.create_dataset("accepted", data=backend.accepted)
|
|
718
|
+
g.create_dataset("log_prob", data=backend.log_prob)
|
|
719
|
+
has_blobs = backend.blobs is not None
|
|
720
|
+
g.attrs["has_blobs"] = has_blobs
|
|
721
|
+
if has_blobs:
|
|
722
|
+
g.create_dataset("blobs", data=backend.blobs)
|
|
723
|
+
g.attrs["iteration"] = backend.iteration
|
|
724
|
+
hf.flush()
|
|
725
|
+
h5_bytes = bytes(hf.id.get_file_image())
|
|
726
|
+
|
|
727
|
+
return _notebook(cells), csv_df, h5_bytes
|