bindmc 0.1.6__tar.gz → 0.1.7__tar.gz

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 (42) hide show
  1. {bindmc-0.1.6 → bindmc-0.1.7}/PKG-INFO +12 -2
  2. {bindmc-0.1.6 → bindmc-0.1.7}/README.md +10 -0
  3. {bindmc-0.1.6 → bindmc-0.1.7}/pyproject.toml +2 -2
  4. {bindmc-0.1.6 → bindmc-0.1.7}/src/bindmc/webgui/classes/ExptData.py +1 -1
  5. {bindmc-0.1.6 → bindmc-0.1.7}/src/bindmc/webgui/classes/FitResult.py +0 -4
  6. {bindmc-0.1.6 → bindmc-0.1.7}/src/bindmc/webgui/components/data_model.py +102 -94
  7. {bindmc-0.1.6 → bindmc-0.1.7}/src/bindmc/webgui/components/fitting.py +11 -86
  8. {bindmc-0.1.6 → bindmc-0.1.7}/src/bindmc/webgui/export/notebook_exporter.py +26 -18
  9. {bindmc-0.1.6 → bindmc-0.1.7}/src/bindmc/webgui/state/statemanager.py +4 -5
  10. {bindmc-0.1.6 → bindmc-0.1.7}/src/bindmc/__init__.py +0 -0
  11. {bindmc-0.1.6 → bindmc-0.1.7}/src/bindmc/__main__.py +0 -0
  12. {bindmc-0.1.6 → bindmc-0.1.7}/src/bindmc/main.py +0 -0
  13. {bindmc-0.1.6 → bindmc-0.1.7}/src/bindmc/webgui/Class model.md +0 -0
  14. {bindmc-0.1.6 → bindmc-0.1.7}/src/bindmc/webgui/TODO.md +0 -0
  15. {bindmc-0.1.6 → bindmc-0.1.7}/src/bindmc/webgui/TODO_old.md +0 -0
  16. {bindmc-0.1.6 → bindmc-0.1.7}/src/bindmc/webgui/__init__.py +0 -0
  17. {bindmc-0.1.6 → bindmc-0.1.7}/src/bindmc/webgui/app.py +0 -0
  18. {bindmc-0.1.6 → bindmc-0.1.7}/src/bindmc/webgui/classes/BindingConstant.py +0 -0
  19. {bindmc-0.1.6 → bindmc-0.1.7}/src/bindmc/webgui/classes/ChemicalShiftParam.py +0 -0
  20. {bindmc-0.1.6 → bindmc-0.1.7}/src/bindmc/webgui/classes/Component.py +0 -0
  21. {bindmc-0.1.6 → bindmc-0.1.7}/src/bindmc/webgui/classes/ExptDataType.py +0 -0
  22. {bindmc-0.1.6 → bindmc-0.1.7}/src/bindmc/webgui/classes/MCMCSim.py +0 -0
  23. {bindmc-0.1.6 → bindmc-0.1.7}/src/bindmc/webgui/classes/Model.py +0 -0
  24. {bindmc-0.1.6 → bindmc-0.1.7}/src/bindmc/webgui/classes/RawData.py +0 -0
  25. {bindmc-0.1.6 → bindmc-0.1.7}/src/bindmc/webgui/classes/Simulation.py +0 -0
  26. {bindmc-0.1.6 → bindmc-0.1.7}/src/bindmc/webgui/classes/UIBindings.py +0 -0
  27. {bindmc-0.1.6 → bindmc-0.1.7}/src/bindmc/webgui/classes/__init__.py +0 -0
  28. {bindmc-0.1.6 → bindmc-0.1.7}/src/bindmc/webgui/components/__init__.py +0 -0
  29. {bindmc-0.1.6 → bindmc-0.1.7}/src/bindmc/webgui/components/base.py +0 -0
  30. {bindmc-0.1.6 → bindmc-0.1.7}/src/bindmc/webgui/components/bayes.py +0 -0
  31. {bindmc-0.1.6 → bindmc-0.1.7}/src/bindmc/webgui/components/bayes_priors.py +0 -0
  32. {bindmc-0.1.6 → bindmc-0.1.7}/src/bindmc/webgui/components/binding_model.py +0 -0
  33. {bindmc-0.1.6 → bindmc-0.1.7}/src/bindmc/webgui/components/body.py +0 -0
  34. {bindmc-0.1.6 → bindmc-0.1.7}/src/bindmc/webgui/components/data_gen.py +0 -0
  35. {bindmc-0.1.6 → bindmc-0.1.7}/src/bindmc/webgui/components/data_import.py +0 -0
  36. {bindmc-0.1.6 → bindmc-0.1.7}/src/bindmc/webgui/components/graph.py +0 -0
  37. {bindmc-0.1.6 → bindmc-0.1.7}/src/bindmc/webgui/components/header.py +0 -0
  38. {bindmc-0.1.6 → bindmc-0.1.7}/src/bindmc/webgui/components/simulation.py +0 -0
  39. {bindmc-0.1.6 → bindmc-0.1.7}/src/bindmc/webgui/default_models.json +0 -0
  40. {bindmc-0.1.6 → bindmc-0.1.7}/src/bindmc/webgui/export/__init__.py +0 -0
  41. {bindmc-0.1.6 → bindmc-0.1.7}/src/bindmc/webgui/state/__init__.py +0 -0
  42. {bindmc-0.1.6 → bindmc-0.1.7}/src/bindmc/webgui/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: bindmc
3
- Version: 0.1.6
3
+ Version: 0.1.7
4
4
  Keywords: chemistry,analytical chemistry,binding constants,supramolecular
5
5
  Author: Martin Peeks
6
6
  Author-email: Martin Peeks <martinp23@googlemail.com>, m.peeks@unsw.edu.au
@@ -12,7 +12,7 @@ Classifier: Programming Language :: Python :: 3.14
12
12
  Classifier: Topic :: Scientific/Engineering :: Chemistry
13
13
  Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
14
14
  Requires-Dist: arviz==0.21.0
15
- Requires-Dist: bindtools>=0.2.1
15
+ Requires-Dist: bindtools>=0.2.2
16
16
  Requires-Dist: corner==2.2.3
17
17
  Requires-Dist: emcee==3.1.6
18
18
  Requires-Dist: h5py>=3.14.0
@@ -53,6 +53,16 @@ Download the latest executable for your platform from the [Releases](https://git
53
53
  pip install bindmc
54
54
  ```
55
55
 
56
+ ## Upgrades
57
+
58
+ ### Pre-built Binary
59
+ Re-download from the link above.
60
+
61
+ ### Pip
62
+ ```bash
63
+ pip install --upgrade bindmc
64
+ ```
65
+
56
66
  ## Usage
57
67
 
58
68
  If using the pre-built binary, run the downloaded executable.
@@ -15,6 +15,16 @@ Download the latest executable for your platform from the [Releases](https://git
15
15
  pip install bindmc
16
16
  ```
17
17
 
18
+ ## Upgrades
19
+
20
+ ### Pre-built Binary
21
+ Re-download from the link above.
22
+
23
+ ### Pip
24
+ ```bash
25
+ pip install --upgrade bindmc
26
+ ```
27
+
18
28
  ## Usage
19
29
 
20
30
  If using the pre-built binary, run the downloaded executable.
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "bindmc"
7
- version = "0.1.6"
7
+ version = "0.1.7"
8
8
  readme = "README.md"
9
9
  keywords = ["chemistry", "analytical chemistry", "binding constants", "supramolecular"]
10
10
  classifiers = [
@@ -28,7 +28,7 @@ classifiers = [
28
28
  requires-python = ">=3.12"
29
29
  dependencies = [
30
30
  "arviz==0.21.0",
31
- "bindtools>=0.2.1",
31
+ "bindtools>=0.2.2",
32
32
  "corner==2.2.3",
33
33
  "emcee==3.1.6",
34
34
  "h5py>=3.14.0",
@@ -443,7 +443,7 @@ class ExptData:
443
443
  species_key = species_names[j]
444
444
  parameter_matrix[i, j] = _get_parameter_for_species(species_key, delta_col)
445
445
  else:
446
- parameter_matrix[i, j] = 0.0
446
+ parameter_matrix[i, j] = None
447
447
 
448
448
  self.delta_to_spec = parameter_matrix
449
449
  return parameter_matrix
@@ -30,8 +30,6 @@ class FitResult:
30
30
  bd_model: Optional[bd.bindingModel] = None
31
31
  analytical_fast_exchange: bool = False
32
32
  analytical_topology: Optional[str] = None
33
- analytical_obs_columns: list[str] = field(default_factory=list)
34
- analytical_obs_components: list[int] = field(default_factory=list)
35
33
  analytical_complex_indices: list[int] = field(default_factory=list)
36
34
 
37
35
  init_model: InitVar[Optional[Model]] = None # The model used for the fit, if any
@@ -150,8 +148,6 @@ class FitResult:
150
148
  "success": self.success,
151
149
  "analytical_fast_exchange": self.analytical_fast_exchange,
152
150
  "analytical_topology": self.analytical_topology,
153
- "analytical_obs_columns": list(self.analytical_obs_columns),
154
- "analytical_obs_components": list(self.analytical_obs_components),
155
151
  "analytical_complex_indices": list(self.analytical_complex_indices),
156
152
  "fit_speciation": (
157
153
  self.fit_speciation.to_dict(orient="list") if isinstance(self.fit_speciation, pd.DataFrame) else {}
@@ -140,14 +140,15 @@ class DataModelPanel(BaseComponent):
140
140
 
141
141
  with self.dataModel_specInteg_block:
142
142
  self.spec_integ_inps: dict[str, ui.input] = {}
143
+ self.spec_integ_cbs: dict[str, ui.checkbox] = {}
143
144
  for i, spec in enumerate(self.sm.species):
144
145
  with ui.row().classes("items-center"):
145
146
  ui.label(f"Species conc. [{spec}]_free:")
146
147
  self.spec_integ_inps[spec] = ui.input().classes("flex-1").props("clearable")
147
148
 
148
149
  self.spec_integ_inps[spec].on("blur", lambda c=self.spec_integ_inps[spec]: self.set_focus(c))
149
- b = ui.checkbox("Enabled", value=True).props(f"testid=spec-enabled-{spec}")
150
- self.spec_integ_inps[spec].bind_enabled_from(b, "value")
150
+ self.spec_integ_cbs[spec] = ui.checkbox("Enabled", value=True).props(f"testid=spec-enabled-{spec}")
151
+ self.spec_integ_inps[spec].bind_enabled_from(self.spec_integ_cbs[spec], "value")
151
152
  if (
152
153
  hasattr(active_expt, "integ_to_spec")
153
154
  and active_expt.integ_to_spec is not None
@@ -157,9 +158,9 @@ class DataModelPanel(BaseComponent):
157
158
  active_expt.integ_to_spec[i], active_expt.columns
158
159
  )
159
160
  if self.spec_integ_inps[spec].value == "":
160
- b.value = False
161
+ self.spec_integ_cbs[spec].value = False
161
162
  else:
162
- b.value = False
163
+ self.spec_integ_cbs[spec].value = False
163
164
 
164
165
  def _gen_spec_fast_exchange_block(self):
165
166
  """Generate the species fast exchange block.
@@ -227,95 +228,94 @@ class DataModelPanel(BaseComponent):
227
228
  self.fast_ex_chem_shift_params = []
228
229
  # map per spec-delta index -> { species_name: {card, shift_num, fixed_cb, min_num, max_num} }
229
230
  self.fast_ex_chem_shift_map = []
231
+ self.specDeltaCbs = []
230
232
 
231
233
  # keep the fast-exchange column names for later processing and bindings
232
234
  self.fast_ex_list_names = list(fast_ex_list)
233
- if len(extra_deps) > 0:
234
- with self.dataModel_specFastExchange_block:
235
- # species_list = [f'{x}_free' for x in self.sm.species]
236
-
237
- # default_shift_species = _default_analytical_shift_species(
238
- # self.sm.active_model.eq_mat,
239
- # list(self.sm.active_model.component_names),
240
- # species_list,
241
- # len(self.fast_ex_list_names),
242
- # )
243
- # if default_shift_species is not None:
244
- # ui.label(
245
- # f"Analytical model detected: defaulting fast-exchange expression to [{default_shift_species}] "
246
- # "for this observable. You can edit it if needed."
247
- # ).classes("text-xs text-gray-600")
248
-
249
- for i, shift in enumerate(self.fast_ex_list_names):
250
- # card per chemical shift column
251
- card = ui.card().classes("mb-2")
252
- self.specDeltaCards.append(card)
253
- with card:
254
- ui.label(f"Fast exchange shift {i + 1} ({shift})").classes("text-sm font-semibold")
255
- with ui.row().classes("items-center"):
256
- inp = ui.input().classes("flex-1").props("clearable")
257
- self.specDeltaInps.append(inp)
258
- # species chips row will be added below the input to allow quick insertion
259
- with ui.row().classes("gap-1 mt-2"):
260
- for text in [f"{x}_free" for x in self.sm.species]:
261
- ui.chip(
262
- text,
263
- color="teal",
264
- on_click=lambda h=text: self._insert_species_into_fast_inp(h),
265
- )
266
- # placeholder checkbox to enable the input
267
- en_cb = ui.checkbox("Enabled", value=True)
268
- inp.bind_enabled_from(en_cb, "value")
269
- # placeholder param block element (initially empty/hidden)
270
- pb = ui.element()
271
- self.fast_ex_chem_shift_blocks.append(pb)
272
- self.fast_ex_chem_shift_map.append({})
273
- # store param dict for later
274
- # self.fast_ex_chem_shift_params.append({'shift': None, 'fixed': False, 'min': None, 'max': None})
275
-
276
- # bind blur handler to create/update parameter block
277
- # use default args to capture current inp and index
278
- # allow chips/clicks to insert into this input by remembering last focus
279
- inp.on("focus", lambda e, widget=inp: self.set_focus(widget))
280
- inp.on("click", lambda e, widget=inp: self.set_focus(widget))
281
- # handle blur to create/update parameter sub-blocks
282
- inp.on("blur", lambda e, idx=i, widget=inp: self._handle_spec_delta_blur(idx, widget))
283
- # immediate change handler: reparse on every change
284
- inp.on_value_change(lambda e, idx=i, widget=inp: self._handle_spec_delta_blur(idx, widget))
285
-
286
- # if there is saved delta_to_spec data, populate the input
287
- if (
288
- hasattr(active_expt, "delta_to_spec")
289
- and active_expt.delta_to_spec is not None
290
- and len(active_expt.delta_to_spec) > 0
291
- ):
292
- delta_for_eqn = active_expt.delta_to_spec[i].copy()
293
-
294
- for ij, el in enumerate(delta_for_eqn):
295
- if np.isclose(el, 0):
296
- delta_for_eqn[ij] = 0
297
- else:
298
- if (f"{self.sm.species[ij]}_free", shift) in active_expt.limiting_shifts:
299
- s = active_expt.limiting_shifts[f"{self.sm.species[ij]}_free", shift]
300
- if s.value:
301
- delta_for_eqn[ij] = delta_for_eqn[ij] / s.value
302
- else:
303
- delta_for_eqn[ij] = 1
304
-
305
- # self.sm.active_expt_data.limiting_shifts[f'{self.sm.species[i]}_free',shift].value
306
-
307
- # delta_for_eqn[delta_for_eqn != 0] = 1
308
-
309
- value = self.vec_to_conc_expression(
310
- delta_for_eqn, [f"{x}_free" for x in self.sm.species]
235
+ with self.dataModel_specFastExchange_block:
236
+ # species_list = [f'{x}_free' for x in self.sm.species]
237
+
238
+ default_shift_species = None
239
+ if simple_model is not None and len(simple_model[1]) > 0:
240
+ default_shift_species = f"{self.sm.species[simple_model[1][0]]}_free"
241
+
242
+ if default_shift_species is not None:
243
+ ui.label(
244
+ f"Analytical model detected: defaulting fast-exchange expression to [{default_shift_species}] "
245
+ "for this observable. You can edit it if needed."
246
+ ).classes("text-xs text-gray-600")
247
+
248
+ for i, shift in enumerate(self.fast_ex_list_names):
249
+ # card per chemical shift column
250
+ card = ui.card().classes("mb-2")
251
+ self.specDeltaCards.append(card)
252
+ with card:
253
+ ui.label(f"Fast exchange shift {i + 1} ({shift})").classes("text-sm font-semibold")
254
+ with ui.row().classes("items-center"):
255
+ inp = ui.input().classes("flex-1").props("clearable")
256
+ self.specDeltaInps.append(inp)
257
+ # species chips row will be added below the input to allow quick insertion
258
+ with ui.row().classes("gap-1 mt-2"):
259
+ for text in [f"{x}_free" for x in self.sm.species]:
260
+ ui.chip(
261
+ text,
262
+ color="teal",
263
+ on_click=lambda h=text: self._insert_species_into_fast_inp(h),
311
264
  )
312
- inp.value = value
313
- if value == "":
314
- en_cb.value = False
315
- # elif default_shift_species is not None:
316
- # inp.value = f"[{default_shift_species}]"
317
- # # Build corresponding ChemicalShiftParam widgets immediately.
318
- # self._handle_spec_delta_blur(i, inp)
265
+ # placeholder checkbox to enable the input
266
+ en_cb = ui.checkbox("Enabled", value=True)
267
+ self.specDeltaCbs.append(en_cb)
268
+ inp.bind_enabled_from(en_cb, "value")
269
+ # placeholder param block element (initially empty/hidden)
270
+ pb = ui.element()
271
+ self.fast_ex_chem_shift_blocks.append(pb)
272
+ self.fast_ex_chem_shift_map.append({})
273
+ # store param dict for later
274
+ # self.fast_ex_chem_shift_params.append({'shift': None, 'fixed': False, 'min': None, 'max': None})
275
+
276
+ # bind blur handler to create/update parameter block
277
+ # use default args to capture current inp and index
278
+ # allow chips/clicks to insert into this input by remembering last focus
279
+ inp.on("focus", lambda e, widget=inp: self.set_focus(widget))
280
+ inp.on("click", lambda e, widget=inp: self.set_focus(widget))
281
+ # handle blur to create/update parameter sub-blocks
282
+ inp.on("blur", lambda e, idx=i, widget=inp: self._handle_spec_delta_blur(idx, widget))
283
+ # immediate change handler: reparse on every change
284
+ inp.on_value_change(lambda e, idx=i, widget=inp: self._handle_spec_delta_blur(idx, widget))
285
+
286
+ # if there is saved delta_to_spec data, populate the input
287
+ if (
288
+ hasattr(active_expt, "delta_to_spec")
289
+ and active_expt.delta_to_spec is not None
290
+ and len(active_expt.delta_to_spec) > 0
291
+ ):
292
+ delta_for_eqn = active_expt.delta_to_spec[i].copy()
293
+
294
+ for ij, el in enumerate(delta_for_eqn):
295
+ if np.isclose(el, 0):
296
+ delta_for_eqn[ij] = 0
297
+ else:
298
+ if (f"{self.sm.species[ij]}_free", shift) in active_expt.limiting_shifts:
299
+ s = active_expt.limiting_shifts[f"{self.sm.species[ij]}_free", shift]
300
+ if s.value:
301
+ delta_for_eqn[ij] = delta_for_eqn[ij] / s.value
302
+ else:
303
+ delta_for_eqn[ij] = 1
304
+
305
+ # self.sm.active_expt_data.limiting_shifts[f'{self.sm.species[i]}_free',shift].value
306
+
307
+ # delta_for_eqn[delta_for_eqn != 0] = 1
308
+
309
+ value = self.vec_to_conc_expression(
310
+ delta_for_eqn, [f"{x}_free" for x in self.sm.species]
311
+ )
312
+ inp.value = value
313
+ if value == "":
314
+ en_cb.value = False
315
+ elif default_shift_species is not None:
316
+ inp.value = f"[{default_shift_species}]"
317
+ # Build corresponding ChemicalShiftParam widgets immediately.
318
+ self._handle_spec_delta_blur(i, inp)
319
319
 
320
320
  # finished generating fast-exchange block
321
321
 
@@ -371,6 +371,14 @@ class DataModelPanel(BaseComponent):
371
371
  if not isinstance(cs_obj, ChemicalShiftParam):
372
372
  # create with safe defaults
373
373
  cs_obj = ChemicalShiftParam(species=spec_name, col=col_name, fixed=False)
374
+ try:
375
+ if hasattr(active_expt, "data") and active_expt.data is not None and col_name in active_expt.data.columns:
376
+ first_val = float(active_expt.data[col_name].dropna().iloc[0])
377
+ cs_obj.value = round(first_val, 4)
378
+ cs_obj._min = round(first_val - 1.0, 4)
379
+ cs_obj._max = round(first_val + 1.0, 4)
380
+ except Exception:
381
+ pass
374
382
  active_expt.limiting_shifts[k] = cs_obj
375
383
 
376
384
  # reuse if exists
@@ -463,10 +471,10 @@ class DataModelPanel(BaseComponent):
463
471
  target.integ_to_spec = None
464
472
  else:
465
473
  integ_to_spec = [
466
- self.conc_expression_to_vec(input.value, target.columns)
467
- if input.enabled
474
+ self.conc_expression_to_vec(self.spec_integ_inps[spec].value, target.columns)
475
+ if self.spec_integ_cbs[spec].value
468
476
  else np.zeros(len(target.columns))
469
- for input in self.spec_integ_inps.values()
477
+ for spec in self.sm.species
470
478
  ]
471
479
  integ_to_spec = np.array(integ_to_spec)
472
480
  if integ_to_spec.size == 0 or np.all(np.isclose(integ_to_spec, 0)):
@@ -483,9 +491,9 @@ class DataModelPanel(BaseComponent):
483
491
  if hasattr(self, "specDeltaInps") and isinstance(self.specDeltaInps, list) and len(self.specDeltaInps) > 0:
484
492
  species_label_list = [f"{s}_free" for s in self.sm.species]
485
493
  for idx, input_widget in enumerate(self.specDeltaInps):
494
+ cb = self.specDeltaCbs[idx]
486
495
  if (
487
- hasattr(input_widget, "enabled")
488
- and input_widget.enabled
496
+ cb.value
489
497
  and isinstance(input_widget.value, str)
490
498
  and input_widget.value.strip()
491
499
  ):
@@ -132,8 +132,6 @@ def _infer_analytical_fast_exchange_config(model, expt_data, expt_dtypes: dict)
132
132
  return {
133
133
  "topology": topo_name,
134
134
  "complex_indices": complex_indices,
135
- "obs_columns": [], # no NMR shift columns
136
- "obs_components": [],
137
135
  }
138
136
 
139
137
  # Pure NMR shift path (existing behaviour).
@@ -146,85 +144,9 @@ def _infer_analytical_fast_exchange_config(model, expt_data, expt_dtypes: dict)
146
144
  if finite.size > 0 and np.any(~np.isclose(finite, 0.0)):
147
145
  return None
148
146
 
149
- component_free_labels = [f"{name}_free" for name in model.component_names]
150
- if len(component_free_labels) != 2:
151
- return None
152
-
153
- # Build optional hints from existing user mappings when available.
154
- shift_species_by_col: dict[str, set[str]] = {}
155
- if isinstance(expt_data.limiting_shifts, dict):
156
- for (species, col_name), _ in expt_data.limiting_shifts.items():
157
- if col_name is None:
158
- continue
159
- shift_species_by_col.setdefault(str(col_name), set()).add(str(species))
160
-
161
- delta_species_hints: dict[str, set[str]] = {}
162
- delta_to_spec = expt_data.delta_to_spec
163
- if isinstance(delta_to_spec, np.ndarray) and delta_to_spec.ndim == 2 and delta_to_spec.size > 0:
164
- # In this UI flow, delta_to_spec rows are created from fast-exchange observable columns.
165
- # For analytical mode all dependent observables are shift observables, so the row order
166
- # corresponds to obs_list.
167
- n_rows = min(delta_to_spec.shape[0], len(obs_list))
168
- n_species = min(delta_to_spec.shape[1], len(component_free_labels))
169
- for ridx in range(n_rows):
170
- col_name = obs_list[ridx]
171
- for sidx in range(n_species):
172
- try:
173
- is_nonzero = not np.isclose(float(delta_to_spec[ridx, sidx]), 0.0)
174
- except Exception:
175
- is_nonzero = bool(delta_to_spec[ridx, sidx])
176
- if is_nonzero:
177
- delta_species_hints.setdefault(col_name, set()).add(component_free_labels[sidx])
178
-
179
- def _component_from_text_hints(col_name: str, dtype_key: str | None) -> int | None:
180
- # Tokenize to avoid over-matching short component names in arbitrary strings.
181
- tokens = []
182
- for src in (col_name, dtype_key or ""):
183
- parts = [t for t in re.split(r"[^A-Za-z0-9]+", str(src).lower()) if t]
184
- tokens.extend(parts)
185
- matches = [idx for idx, comp in enumerate(model.component_names) if str(comp).lower() in tokens]
186
- return matches[0] if len(matches) == 1 else None
187
-
188
- obs_components: list[int] = []
189
- unresolved: list[tuple[int, str]] = []
190
- for obs_idx, col in enumerate(obs_list):
191
- col_meta = expt_data.col_details.get(col, {})
192
- dtype_key = col_meta.get("dtype")
193
-
194
- included_species = set()
195
- included_species |= shift_species_by_col.get(col, set())
196
- included_species |= delta_species_hints.get(col, set())
197
-
198
- has_comp0 = component_free_labels[0] in included_species
199
- has_comp1 = component_free_labels[1] in included_species
200
- if has_comp0 != has_comp1:
201
- obs_components.append(0 if has_comp0 else 1)
202
- continue
203
-
204
- inferred = _component_from_text_hints(col, dtype_key)
205
- if inferred is not None:
206
- obs_components.append(inferred)
207
- continue
208
-
209
- unresolved.append((obs_idx, col))
210
- obs_components.append(-1)
211
-
212
- # Final fallback keeps analytical mode enabled even without manual shift metadata.
213
- if unresolved:
214
- for obs_idx, col in unresolved:
215
- fallback = obs_idx % len(component_free_labels)
216
- obs_components[obs_idx] = fallback
217
- logger.info(
218
- "Analytical fast-exchange: inferred observable '%s' as component %d by default fallback.",
219
- col,
220
- fallback,
221
- )
222
-
223
147
  return {
224
148
  "topology": topo_name,
225
149
  "complex_indices": complex_indices,
226
- "obs_columns": list(obs_list),
227
- "obs_components": obs_components,
228
150
  }
229
151
 
230
152
 
@@ -237,9 +159,9 @@ class FittingPanel(BaseComponent):
237
159
  ui.label("Fitting panel").classes("text-lg font-bold mb-4")
238
160
  with ui.row().classes("w-full gap-4 items-start flex-col lg:flex-row"):
239
161
  with ui.card().classes("w-full lg:w-80 shrink-0"):
240
- ui.label("Fitting options to go here.")
162
+ # ui.label("Fitting options to go here.")
241
163
  self.fit_alg_select = ui.select(
242
- ["least_squares", "l-bfgs", "ampgo"],
164
+ ["least_squares", "l-bfgs", "l-bfgs-b", "nelder-mead", "ampgo"],
243
165
  label="Algorithm",
244
166
  on_change=lambda e: print(f"Selected: {e.value}"),
245
167
  value="least_squares",
@@ -494,8 +416,6 @@ class FittingPanel(BaseComponent):
494
416
  bd_model=self.m1,
495
417
  analytical_fast_exchange=self.m1.analytical_fast_exchange,
496
418
  analytical_topology=self.m1.analytical_topology,
497
- analytical_obs_columns=[str(x) for x in self.m1.analytical_obs_columns],
498
- analytical_obs_components=[int(x) for x in self.m1.analytical_obs_components],
499
419
  analytical_complex_indices=self.m1.analytical_complex_indices,
500
420
  )
501
421
  self.sm.add_fit(new_fit)
@@ -510,6 +430,11 @@ class FittingPanel(BaseComponent):
510
430
  "initial_value": v.init_value,
511
431
  }
512
432
 
433
+ print("DEBUG: specToInteg =", self.sm.active_expt_data.integ_to_spec)
434
+ print("DEBUG: specToDd =", self.sm.active_expt_data.delta_to_spec)
435
+ print("DEBUG: obsList =", self.m1.obsList)
436
+ print("DEBUG: calc_obs shape =", np.shape(calc_obs))
437
+
513
438
  self.sm.active_fit.calc_obs = pd.DataFrame(calc_obs, columns=self.m1.obsList)
514
439
 
515
440
  self.sm.active_fit.fit_speciation = pd.DataFrame(speciation, columns=[x + "_free" for x in self.sm.species])
@@ -800,7 +725,7 @@ class FitResultsCard(BaseComponent):
800
725
  for fit in self.sm.fits.values():
801
726
  row = {
802
727
  "name": fit.name,
803
- "chisqr": self.rounded_value(fit.chisqr),
728
+ "chisqr": self.rounded_value(fit.chisqr,scientific=True),
804
729
  "aic": self.rounded_value(fit.aic, dp=1),
805
730
  "bic": self.rounded_value(fit.bic, dp=1),
806
731
  "message": fit.termination_message,
@@ -817,10 +742,10 @@ class FitResultsCard(BaseComponent):
817
742
  self.add_body_slot()
818
743
  self.table.update()
819
744
 
820
- def rounded_value(self, value: float, dp: int = 3) -> str:
745
+ def rounded_value(self, value: float, dp: int = 3, scientific=False) -> str:
821
746
  """Round the value for display."""
822
- if abs(value) >= 10000 or abs(value) < 0.001:
823
- return f"{value:.4e}"
747
+ if abs(value) >= 10000 or abs(value) < 0.001 or scientific:
748
+ return f"{value:.3e}"
824
749
  else:
825
750
  return f"{value:.{dp}f}" if isinstance(value, float) else str(value)
826
751
 
@@ -239,17 +239,7 @@ def _build_lin_obs_cell4_lines(
239
239
  return lines
240
240
 
241
241
 
242
- def _build_analytical_lin_obs_lines(lin_obs_param_map: list) -> list[str]:
243
- """Return setup lines for the analytical path's linear_obs_param_map.
244
242
 
245
- Extracts the param name (or None) from each cell so
246
- fitfun_analytical_fast_exchange can call calc_analytical_linear_observables.
247
- """
248
- # Serialise as [[name_or_none, ...], ...]
249
- name_map = [[cell["name"] if cell is not None else None for cell in row] for row in lin_obs_param_map]
250
- return [
251
- f"m.analytical_linear_obs_param_map = {name_map!r}",
252
- ]
253
243
 
254
244
 
255
245
  def export_fit_notebook(
@@ -382,7 +372,31 @@ def export_fit_notebook(
382
372
  spec_to_integ_arg = "None"
383
373
 
384
374
  if has_delta:
385
- spec_to_dd_line = f"spec_to_dd = np.array({delta_to_spec.T.tolist()!r}, dtype=object)"
375
+ # Build spec_to_dd reconstruction lines dynamically to properly instantiate lmfit.Parameters
376
+ spec_to_dd_lines = [
377
+ "spec_to_dd = np.empty((len(species_names), len(obs_list)), dtype=object)"
378
+ ]
379
+ delta_T = delta_to_spec.T
380
+ for i in range(delta_T.shape[0]):
381
+ for j in range(delta_T.shape[1]):
382
+ cell = delta_T[i, j]
383
+ if cell is None:
384
+ spec_to_dd_lines.append(f"spec_to_dd[{i}, {j}] = None")
385
+ elif isinstance(cell, (int, float, np.floating)):
386
+ if np.isnan(cell):
387
+ spec_to_dd_lines.append(f"spec_to_dd[{i}, {j}] = None")
388
+ else:
389
+ spec_to_dd_lines.append(f"spec_to_dd[{i}, {j}] = {float(cell)!r}")
390
+ else: # lmfit.Parameter
391
+ pname = getattr(cell, "name", "")
392
+ pval = getattr(cell, "value", 0.0)
393
+ pmin = getattr(cell, "min", -np.inf)
394
+ pmax = getattr(cell, "max", np.inf)
395
+ pvary = getattr(cell, "vary", True)
396
+ spec_to_dd_lines.append(
397
+ f"spec_to_dd[{i}, {j}] = lmfit.Parameter({pname!r}, value={pval!r}, min={pmin!r}, max={pmax!r}, vary={pvary!r})"
398
+ )
399
+ spec_to_dd_line = "\n".join(spec_to_dd_lines)
386
400
  spec_to_dd_arg = "spec_to_dd"
387
401
  else:
388
402
  spec_to_dd_line = "spec_to_dd = None"
@@ -390,8 +404,6 @@ def export_fit_notebook(
390
404
 
391
405
  is_analytical = bool(getattr(fit, "analytical_fast_exchange", False))
392
406
  analytical_topology = getattr(fit, "analytical_topology", None)
393
- analytical_obs_columns = list(getattr(fit, "analytical_obs_columns", []))
394
- analytical_obs_components = list(getattr(fit, "analytical_obs_components", []))
395
407
  analytical_complex_indices = list(getattr(fit, "analytical_complex_indices", []))
396
408
 
397
409
  has_lin_obs = bool(lin_obs_col_names and lin_obs_param_map)
@@ -399,15 +411,11 @@ def export_fit_notebook(
399
411
  if is_analytical:
400
412
  analytical_setup_lines: list[str] = [
401
413
  "",
402
- "# Analytical fast-exchange backend — must be configured before prepModel()",
414
+ "# Analytical fast-exchange speciation",
403
415
  "m.analytical_fast_exchange = True",
404
416
  f"m.analytical_topology = {analytical_topology!r}",
405
- f"m.analytical_obs_columns = {analytical_obs_columns!r}",
406
- f"m.analytical_obs_components = {analytical_obs_components!r}",
407
417
  f"m.analytical_complex_indices = {analytical_complex_indices!r}",
408
418
  ]
409
- if has_lin_obs:
410
- analytical_setup_lines += _build_analytical_lin_obs_lines(lin_obs_param_map) # type: ignore[arg-type]
411
419
  else:
412
420
  analytical_setup_lines = []
413
421
 
@@ -1167,6 +1167,9 @@ class StateManager:
1167
1167
  fittemp = data.get("fits", []) # List of fits
1168
1168
  if fittemp:
1169
1169
  for fit in fittemp:
1170
+ # backward compatibility to load pre-0.1.7 fits
1171
+ fit.pop("analytical_obs_columns",None)
1172
+ fit.pop("analytical_obs_components",None)
1170
1173
  fit_result = FitResult(**fit)
1171
1174
  fit_result.fit_speciation = pd.DataFrame(fit.get("fit_speciation", {}))
1172
1175
  fit_result.calc_obs = pd.DataFrame(fit.get("calc_obs", {}))
@@ -1233,7 +1236,7 @@ class StateManager:
1233
1236
  parameter.vary = bool(cell.get("vary", True))
1234
1237
  reconstructed_row.append(parameter)
1235
1238
  elif cell is None:
1236
- reconstructed_row.append(0.0)
1239
+ reconstructed_row.append(None)
1237
1240
  elif isinstance(cell, (str, int, float)):
1238
1241
  try:
1239
1242
  reconstructed_row.append(float(cell))
@@ -1736,16 +1739,12 @@ bd.makeFitResidPlot(fit,plotMask=(0,1),ylabel='Chemical shift (ppm)')"""
1736
1739
  cfg = {
1737
1740
  "topology": fit.analytical_topology,
1738
1741
  "complex_indices": list(getattr(fit, "analytical_complex_indices", [])),
1739
- "obs_columns": list(getattr(fit, "analytical_obs_columns", [])),
1740
- "obs_components": list(getattr(fit, "analytical_obs_components", [])),
1741
1742
  }
1742
1743
 
1743
1744
  if cfg is not None:
1744
1745
  model.analytical_fast_exchange = True
1745
1746
  model.analytical_topology = str(cfg["topology"])
1746
1747
  model.analytical_complex_indices = [int(x) for x in cfg["complex_indices"]] # type: ignore[index]
1747
- model.analytical_obs_columns = [str(x) for x in cfg["obs_columns"]] # type: ignore[index]
1748
- model.analytical_obs_components = [int(x) for x in cfg["obs_components"]] # type: ignore[index]
1749
1748
  logger.info(
1750
1749
  "Using analytical fast-exchange backend (%s model) for fitting.",
1751
1750
  model.analytical_topology,
File without changes
File without changes
File without changes
File without changes
File without changes