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,2043 @@
|
|
|
1
|
+
import gzip
|
|
2
|
+
import io
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
import uuid
|
|
7
|
+
from dataclasses import asdict
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from typing import Optional, Callable, Any
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
import pandas as pd
|
|
13
|
+
from nicegui import app, ui
|
|
14
|
+
from nicegui.events import UploadEventArguments, ValueChangeEventArguments
|
|
15
|
+
import bindtools.binding as bd
|
|
16
|
+
|
|
17
|
+
from ..classes import BindingConstant, Component, Model, Simulation, ExptData, FitResult, RawData,ExptDataType, ChemicalShiftParam, MCMCSim, UIBindings
|
|
18
|
+
from ..utils import eq_mat_from_equation_str_infer_components
|
|
19
|
+
from lmfit import Parameter as LMFitParameter
|
|
20
|
+
import logging
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
class StateManager:
|
|
24
|
+
|
|
25
|
+
def __init__(self,load_prior_state: bool = True):
|
|
26
|
+
self.models: dict[uuid.UUID,Model] = {} # make these uuid-keyed dicts?
|
|
27
|
+
self.fits: dict[uuid.UUID,FitResult] = {}
|
|
28
|
+
self.expt_datas: dict[uuid.UUID,ExptData] = {}
|
|
29
|
+
self.simulations: dict[uuid.UUID,Simulation] = {}
|
|
30
|
+
self.experimental_data: Optional[pd.DataFrame] = None
|
|
31
|
+
self.raw_datas: dict[uuid.UUID, RawData] = {}
|
|
32
|
+
self.mcmcs: dict[uuid.UUID, MCMCSim] = {}
|
|
33
|
+
self._active_model_id: Optional[uuid.UUID] = None
|
|
34
|
+
self._active_sim_id: Optional[uuid.UUID] = None
|
|
35
|
+
self._active_fit_id: Optional[uuid.UUID] = None
|
|
36
|
+
self._active_expt_data_id: Optional[uuid.UUID] = None
|
|
37
|
+
self._active_raw_data_id: Optional[uuid.UUID] = None
|
|
38
|
+
self._active_mcmc_id: Optional[uuid.UUID] = None
|
|
39
|
+
self._listeners: dict[str, list[Callable]] = {}
|
|
40
|
+
self.default_model_ids: list[uuid.UUID] = []
|
|
41
|
+
self.raw_data: RawData = RawData() # Initialize with an empty RawData instance
|
|
42
|
+
self.ui_bindings: UIBindings = UIBindings()
|
|
43
|
+
|
|
44
|
+
self._expt_dtypes: dict[str,ExptDataType] = {
|
|
45
|
+
'conc': ExptDataType(name='Conc.', init_meas='grav_vol', units='M'),
|
|
46
|
+
'nmr conc': ExptDataType(name='NMR Conc.', init_meas='nmr_integ', units='M'),
|
|
47
|
+
'delta h': ExptDataType(name='H (ppm)',init_meas='nmr_ppm',units='ppm'),
|
|
48
|
+
'delta f': ExptDataType(name='F (ppm)',init_meas='nmr_ppm',units='ppm'),
|
|
49
|
+
'absorbance': ExptDataType(name='Absorbance', init_meas='uvvis', units='absorbance'),
|
|
50
|
+
'fluorescence': ExptDataType(name='Fluorescence int.', init_meas='fluor', units='intensity'),
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
self._init_bindables()
|
|
56
|
+
self.add_listener(
|
|
57
|
+
"model_changed", self.save_to_storage
|
|
58
|
+
) # Save state when model is updated
|
|
59
|
+
self.add_listener(
|
|
60
|
+
"simulation_completed", self.save_to_storage
|
|
61
|
+
) # Save state when simulation is completed
|
|
62
|
+
self.add_listener(
|
|
63
|
+
"fit_completed", self.save_to_storage
|
|
64
|
+
) # Save state when simulation is completed
|
|
65
|
+
self.add_listener(
|
|
66
|
+
"simulation_deleted", self.save_to_storage
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# self._set_initial_simFig_style()
|
|
70
|
+
|
|
71
|
+
if load_prior_state and app.storage.user.get("state-data"):
|
|
72
|
+
try:
|
|
73
|
+
self.load_storage_into_state()
|
|
74
|
+
except Exception as e:
|
|
75
|
+
logger.error(f"Error loading model data: {e}")
|
|
76
|
+
|
|
77
|
+
# Initialize with default model
|
|
78
|
+
if len(self.models) == 0:
|
|
79
|
+
self._initialize_default_models(emit_events=False)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def load_storage_into_state(self):
|
|
83
|
+
|
|
84
|
+
orphans = []
|
|
85
|
+
self.from_json(
|
|
86
|
+
app.storage.user["state-data"]
|
|
87
|
+
) # Load SimData from user storage if available
|
|
88
|
+
logger.info("Loaded model data from user storage.")
|
|
89
|
+
# link simulations (which specify a modelID, not a full model object) to the actual models
|
|
90
|
+
for sim in self.simulations.values():
|
|
91
|
+
try:
|
|
92
|
+
sim.find_and_link_model(
|
|
93
|
+
self.models
|
|
94
|
+
) # TODO: handle case where model is not found (seems unlikely?)
|
|
95
|
+
except(ValueError):
|
|
96
|
+
orphans.append(sim)
|
|
97
|
+
for o in orphans:
|
|
98
|
+
logger.info(f'Simulation {o.id} is orphaned; deleting.')
|
|
99
|
+
self._delete_object_core(o)
|
|
100
|
+
|
|
101
|
+
orphans = []
|
|
102
|
+
for fit in self.fits.values():
|
|
103
|
+
try:
|
|
104
|
+
fit.find_and_link_model(
|
|
105
|
+
self.models)
|
|
106
|
+
fit.find_and_link_expt_data(
|
|
107
|
+
self.expt_datas)
|
|
108
|
+
except(ValueError):
|
|
109
|
+
orphans.append(fit)
|
|
110
|
+
|
|
111
|
+
for expt_data in self.expt_datas.values():
|
|
112
|
+
try:
|
|
113
|
+
expt_data.find_and_link_model(
|
|
114
|
+
self.models)
|
|
115
|
+
expt_data.find_and_link_raw_data(
|
|
116
|
+
self.raw_datas)
|
|
117
|
+
|
|
118
|
+
# Initialize selected_columns after raw data is linked
|
|
119
|
+
if not expt_data.selected_columns and hasattr(expt_data, '_raw_data') and expt_data._raw_data:
|
|
120
|
+
raw_data = expt_data._raw_data.data if hasattr(expt_data._raw_data, 'data') else pd.DataFrame()
|
|
121
|
+
if not raw_data.empty:
|
|
122
|
+
expt_data.selected_columns = raw_data.columns.tolist()
|
|
123
|
+
except(ValueError):
|
|
124
|
+
orphans.append(expt_data)
|
|
125
|
+
|
|
126
|
+
for mcmc in self.mcmcs.values():
|
|
127
|
+
try:
|
|
128
|
+
mcmc.find_and_link_model(self.models)
|
|
129
|
+
mcmc.find_and_link_expt_data(self.expt_datas)
|
|
130
|
+
except(ValueError):
|
|
131
|
+
orphans.append(mcmc)
|
|
132
|
+
|
|
133
|
+
for o in orphans:
|
|
134
|
+
if isinstance(o,ExptData):
|
|
135
|
+
otype='ExptData'
|
|
136
|
+
elif isinstance(o,MCMCSim):
|
|
137
|
+
otype = 'MCMCSim'
|
|
138
|
+
elif isinstance(o,FitResult):
|
|
139
|
+
otype = 'FitResult'
|
|
140
|
+
else:
|
|
141
|
+
otype = "Unknown object"
|
|
142
|
+
logger.warning(f'{otype} {o.id} is orphaned, deleting.')
|
|
143
|
+
self._delete_object_core(o)
|
|
144
|
+
|
|
145
|
+
self._finalize_active_context(reason="load_storage_into_state", emit_events=True)
|
|
146
|
+
# Emit key events so that UI components refresh
|
|
147
|
+
self.notify_listeners("active_context_changed", {})
|
|
148
|
+
self.notify_listeners("model_changed")
|
|
149
|
+
self.notify_listeners("data_imported")
|
|
150
|
+
self.notify_listeners("fits_loaded")
|
|
151
|
+
|
|
152
|
+
def add_listener(self, event: str, callback: Callable):
|
|
153
|
+
"""Add a listener for a specific event."""
|
|
154
|
+
if event not in self._listeners:
|
|
155
|
+
self._listeners[event] = []
|
|
156
|
+
self._listeners[event].append(callback)
|
|
157
|
+
|
|
158
|
+
# model_changed - called from binding_model.py; listened by data_gen
|
|
159
|
+
|
|
160
|
+
def notify_listeners(self, event: ValueChangeEventArguments|str, *args, **kwargs):
|
|
161
|
+
"""Notify all listeners of a specific event."""
|
|
162
|
+
if event in self._listeners and isinstance(event,str):
|
|
163
|
+
logger.info(f"Listener notified: {event}")
|
|
164
|
+
uniq_list = list(set(self._listeners[event])) # Ensure unique callbacks
|
|
165
|
+
for callback in uniq_list:
|
|
166
|
+
callback(*args, **kwargs)
|
|
167
|
+
|
|
168
|
+
def _snapshot_object_ids(self) -> dict[str, set[uuid.UUID]]:
|
|
169
|
+
return {
|
|
170
|
+
"models": set(self.models.keys()),
|
|
171
|
+
"fits": set(self.fits.keys()),
|
|
172
|
+
"simulations": set(self.simulations.keys()),
|
|
173
|
+
"expt_datas": set(self.expt_datas.keys()),
|
|
174
|
+
"raw_datas": set(self.raw_datas.keys()),
|
|
175
|
+
"mcmcs": set(self.mcmcs.keys()),
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
def _emit_collection_events(self, before: dict[str, set[uuid.UUID]], after: dict[str, set[uuid.UUID]], notify_listeners: bool) -> None:
|
|
179
|
+
"""Emit compatibility events for collection-level changes."""
|
|
180
|
+
if not notify_listeners:
|
|
181
|
+
return
|
|
182
|
+
if before["models"] != after["models"]:
|
|
183
|
+
self.notify_listeners("model_changed")
|
|
184
|
+
if before["fits"] != after["fits"]:
|
|
185
|
+
self.notify_listeners("fit_deleted")
|
|
186
|
+
if before["simulations"] != after["simulations"]:
|
|
187
|
+
self.notify_listeners("simulation_deleted")
|
|
188
|
+
if before["expt_datas"] != after["expt_datas"] or before["raw_datas"] != after["raw_datas"]:
|
|
189
|
+
self.notify_listeners("expt_data_changed")
|
|
190
|
+
self.notify_listeners("data_imported")
|
|
191
|
+
if before["mcmcs"] != after["mcmcs"]:
|
|
192
|
+
self.notify_listeners("mcmc_changed")
|
|
193
|
+
|
|
194
|
+
def _latest_id(self, coll: dict[uuid.UUID, Any]) -> uuid.UUID | None:
|
|
195
|
+
return next(reversed(coll), None) if coll else None
|
|
196
|
+
|
|
197
|
+
def _latest_matching_id(self, coll: dict[uuid.UUID, Any], predicate: Callable[[Any], bool]) -> uuid.UUID | None:
|
|
198
|
+
for id, obj in reversed(list(coll.items())):
|
|
199
|
+
if predicate(obj):
|
|
200
|
+
return id
|
|
201
|
+
return None
|
|
202
|
+
|
|
203
|
+
def _normalize_uuid(self, value: uuid.UUID | str | None) -> uuid.UUID | None:
|
|
204
|
+
if value is None:
|
|
205
|
+
return None
|
|
206
|
+
if isinstance(value, str):
|
|
207
|
+
if value in ("", "None"):
|
|
208
|
+
return None
|
|
209
|
+
try:
|
|
210
|
+
return uuid.UUID(value)
|
|
211
|
+
except Exception:
|
|
212
|
+
return None
|
|
213
|
+
if isinstance(value, uuid.UUID):
|
|
214
|
+
return value
|
|
215
|
+
return None
|
|
216
|
+
|
|
217
|
+
def _reconcile_active_ids(self, reason: str = "") -> dict[str, tuple[uuid.UUID | None, uuid.UUID | None]]:
|
|
218
|
+
"""Ensure all active IDs are valid and context-consistent."""
|
|
219
|
+
changes: dict[str, tuple[uuid.UUID | None, uuid.UUID | None]] = {}
|
|
220
|
+
|
|
221
|
+
def _set_active(attr: str, new_value: uuid.UUID | None) -> None:
|
|
222
|
+
old_value = getattr(self, attr)
|
|
223
|
+
if old_value != new_value:
|
|
224
|
+
setattr(self, attr, new_value)
|
|
225
|
+
changes[attr] = (old_value, new_value)
|
|
226
|
+
|
|
227
|
+
if not self.models:
|
|
228
|
+
self._initialize_default_models(emit_events=False)
|
|
229
|
+
_set_active("_active_model_id", self._normalize_uuid(self._active_model_id))
|
|
230
|
+
if self._active_model_id not in self.models:
|
|
231
|
+
_set_active("_active_model_id", self._latest_id(self.models))
|
|
232
|
+
|
|
233
|
+
# Raw data: valid ID or fallback to latest.
|
|
234
|
+
_set_active("_active_raw_data_id", self._normalize_uuid(self._active_raw_data_id))
|
|
235
|
+
if self._active_raw_data_id not in self.raw_datas:
|
|
236
|
+
fallback_raw = None
|
|
237
|
+
maybe_expt_id = self._normalize_uuid(self._active_expt_data_id)
|
|
238
|
+
if maybe_expt_id in self.expt_datas and maybe_expt_id is not None:
|
|
239
|
+
maybe_raw = self._normalize_uuid(self.expt_datas[maybe_expt_id].raw_data_id)
|
|
240
|
+
if maybe_raw in self.raw_datas:
|
|
241
|
+
fallback_raw = maybe_raw
|
|
242
|
+
if fallback_raw is None:
|
|
243
|
+
fallback_raw = self._latest_id(self.raw_datas)
|
|
244
|
+
_set_active("_active_raw_data_id", fallback_raw)
|
|
245
|
+
|
|
246
|
+
# Expt data: prefer active raw data, then active model, then any.
|
|
247
|
+
_set_active("_active_expt_data_id", self._normalize_uuid(self._active_expt_data_id))
|
|
248
|
+
expt_is_valid = self._active_expt_data_id in self.expt_datas
|
|
249
|
+
if expt_is_valid and self._active_raw_data_id is not None and self._active_expt_data_id is not None:
|
|
250
|
+
expt_is_valid = self.expt_datas[self._active_expt_data_id].raw_data_id == self._active_raw_data_id
|
|
251
|
+
if not expt_is_valid:
|
|
252
|
+
candidate = None
|
|
253
|
+
if self._active_raw_data_id is not None:
|
|
254
|
+
candidate = self._latest_matching_id(
|
|
255
|
+
self.expt_datas, lambda d: d.raw_data_id == self._active_raw_data_id
|
|
256
|
+
)
|
|
257
|
+
if candidate is None and self._active_model_id is not None:
|
|
258
|
+
candidate = self._latest_matching_id(
|
|
259
|
+
self.expt_datas, lambda d: d.model_id == self._active_model_id
|
|
260
|
+
)
|
|
261
|
+
if candidate is None:
|
|
262
|
+
candidate = self._latest_id(self.expt_datas)
|
|
263
|
+
_set_active("_active_expt_data_id", candidate)
|
|
264
|
+
|
|
265
|
+
if self._active_raw_data_id is None and self._active_expt_data_id in self.expt_datas and self._active_expt_data_id is not None:
|
|
266
|
+
maybe_raw = self._normalize_uuid(self.expt_datas[self._active_expt_data_id].raw_data_id)
|
|
267
|
+
if maybe_raw in self.raw_datas:
|
|
268
|
+
_set_active("_active_raw_data_id", maybe_raw)
|
|
269
|
+
|
|
270
|
+
# Fits: prefer active model + expt, then expt, then model, then any.
|
|
271
|
+
_set_active("_active_fit_id", self._normalize_uuid(self._active_fit_id))
|
|
272
|
+
fit_is_valid = self._active_fit_id in self.fits
|
|
273
|
+
if fit_is_valid and self._active_fit_id is not None:
|
|
274
|
+
afit = self.fits[self._active_fit_id]
|
|
275
|
+
if self._active_model_id is not None and afit.model_id != self._active_model_id:
|
|
276
|
+
fit_is_valid = False
|
|
277
|
+
if self._active_expt_data_id is not None and afit.expt_data_id != self._active_expt_data_id:
|
|
278
|
+
fit_is_valid = False
|
|
279
|
+
if not fit_is_valid:
|
|
280
|
+
candidate = None
|
|
281
|
+
if self._active_model_id is not None and self._active_expt_data_id is not None:
|
|
282
|
+
candidate = self._latest_matching_id(
|
|
283
|
+
self.fits,
|
|
284
|
+
lambda f: f.model_id == self._active_model_id and f.expt_data_id == self._active_expt_data_id,
|
|
285
|
+
)
|
|
286
|
+
if candidate is None and self._active_expt_data_id is not None:
|
|
287
|
+
candidate = self._latest_matching_id(
|
|
288
|
+
self.fits, lambda f: f.expt_data_id == self._active_expt_data_id
|
|
289
|
+
)
|
|
290
|
+
if candidate is None and self._active_model_id is not None:
|
|
291
|
+
candidate = self._latest_matching_id(
|
|
292
|
+
self.fits, lambda f: f.model_id == self._active_model_id
|
|
293
|
+
)
|
|
294
|
+
if candidate is None:
|
|
295
|
+
candidate = self._latest_id(self.fits)
|
|
296
|
+
_set_active("_active_fit_id", candidate)
|
|
297
|
+
|
|
298
|
+
# Simulations: prefer active model then any.
|
|
299
|
+
_set_active("_active_sim_id", self._normalize_uuid(self._active_sim_id))
|
|
300
|
+
sim_is_valid = self._active_sim_id in self.simulations
|
|
301
|
+
if sim_is_valid and self._active_model_id is not None and self._active_sim_id is not None:
|
|
302
|
+
sim_is_valid = self.simulations[self._active_sim_id].model_id == self._active_model_id
|
|
303
|
+
if not sim_is_valid:
|
|
304
|
+
candidate = None
|
|
305
|
+
if self._active_model_id is not None:
|
|
306
|
+
candidate = self._latest_matching_id(
|
|
307
|
+
self.simulations, lambda s: s.model_id == self._active_model_id
|
|
308
|
+
)
|
|
309
|
+
if candidate is None:
|
|
310
|
+
candidate = self._latest_id(self.simulations)
|
|
311
|
+
_set_active("_active_sim_id", candidate)
|
|
312
|
+
|
|
313
|
+
# MCMC: prefer active model + expt, then expt, then model, then any.
|
|
314
|
+
_set_active("_active_mcmc_id", self._normalize_uuid(self._active_mcmc_id))
|
|
315
|
+
mcmc_is_valid = self._active_mcmc_id in self.mcmcs
|
|
316
|
+
if mcmc_is_valid and self._active_mcmc_id is not None:
|
|
317
|
+
amcmc = self.mcmcs[self._active_mcmc_id]
|
|
318
|
+
if self._active_model_id is not None and amcmc.model_id != self._active_model_id:
|
|
319
|
+
mcmc_is_valid = False
|
|
320
|
+
if self._active_expt_data_id is not None and amcmc.expt_data_id != self._active_expt_data_id:
|
|
321
|
+
mcmc_is_valid = False
|
|
322
|
+
if not mcmc_is_valid:
|
|
323
|
+
candidate = None
|
|
324
|
+
if self._active_model_id is not None and self._active_expt_data_id is not None:
|
|
325
|
+
candidate = self._latest_matching_id(
|
|
326
|
+
self.mcmcs,
|
|
327
|
+
lambda m: m.model_id == self._active_model_id and m.expt_data_id == self._active_expt_data_id,
|
|
328
|
+
)
|
|
329
|
+
if candidate is None and self._active_expt_data_id is not None:
|
|
330
|
+
candidate = self._latest_matching_id(
|
|
331
|
+
self.mcmcs, lambda m: m.expt_data_id == self._active_expt_data_id
|
|
332
|
+
)
|
|
333
|
+
if candidate is None and self._active_model_id is not None:
|
|
334
|
+
candidate = self._latest_matching_id(
|
|
335
|
+
self.mcmcs, lambda m: m.model_id == self._active_model_id
|
|
336
|
+
)
|
|
337
|
+
if candidate is None:
|
|
338
|
+
candidate = self._latest_id(self.mcmcs)
|
|
339
|
+
_set_active("_active_mcmc_id", candidate)
|
|
340
|
+
|
|
341
|
+
if reason and changes != {}:
|
|
342
|
+
logger.info(f"Reconciled active IDs ({reason}): {changes}")
|
|
343
|
+
return changes
|
|
344
|
+
|
|
345
|
+
def _emit_active_context_events(self, changes: dict[str, tuple[uuid.UUID | None, uuid.UUID | None]]) -> None:
|
|
346
|
+
if not changes:
|
|
347
|
+
return
|
|
348
|
+
self.notify_listeners("active_context_changed", changes)
|
|
349
|
+
if "_active_model_id" in changes:
|
|
350
|
+
self.update_ui_bindings(["model_name"])
|
|
351
|
+
self.notify_listeners("model_changed")
|
|
352
|
+
if "_active_expt_data_id" in changes or "_active_raw_data_id" in changes:
|
|
353
|
+
self.notify_listeners("expt_data_changed")
|
|
354
|
+
self.notify_listeners("data_imported")
|
|
355
|
+
if "_active_fit_id" in changes:
|
|
356
|
+
self.notify_listeners("fit_changed")
|
|
357
|
+
if "_active_sim_id" in changes:
|
|
358
|
+
self.notify_listeners("sim_changed")
|
|
359
|
+
|
|
360
|
+
def _finalize_active_context(self, reason: str, emit_events: bool = True) -> dict[str, tuple[uuid.UUID | None, uuid.UUID | None]]:
|
|
361
|
+
changes = self._reconcile_active_ids(reason=reason)
|
|
362
|
+
if emit_events:
|
|
363
|
+
self._emit_active_context_events(changes)
|
|
364
|
+
return changes
|
|
365
|
+
|
|
366
|
+
def reconcile_active_context(self, reason: str = "manual", emit_events: bool = True) -> dict[str, tuple[uuid.UUID | None, uuid.UUID | None]]:
|
|
367
|
+
"""Public wrapper to reconcile and optionally emit active-context events."""
|
|
368
|
+
return self._finalize_active_context(reason=reason, emit_events=emit_events)
|
|
369
|
+
|
|
370
|
+
def _delete_object_core(self, obj: Model | FitResult | Simulation | ExptData | RawData | MCMCSim) -> None:
|
|
371
|
+
"""Delete an object and dependent objects without emitting events."""
|
|
372
|
+
if isinstance(obj, Model):
|
|
373
|
+
if obj.id in self.models:
|
|
374
|
+
del self.models[obj.id]
|
|
375
|
+
if self._active_model_id == obj.id:
|
|
376
|
+
self._active_model_id = None
|
|
377
|
+
elif isinstance(obj, FitResult):
|
|
378
|
+
if obj.id in self.fits:
|
|
379
|
+
del self.fits[obj.id]
|
|
380
|
+
if self._active_fit_id == obj.id:
|
|
381
|
+
self._active_fit_id = None
|
|
382
|
+
elif isinstance(obj, Simulation):
|
|
383
|
+
if obj.id in self.simulations:
|
|
384
|
+
del self.simulations[obj.id]
|
|
385
|
+
if self._active_sim_id == obj.id:
|
|
386
|
+
self._active_sim_id = None
|
|
387
|
+
elif isinstance(obj, ExptData):
|
|
388
|
+
if obj.id in self.expt_datas:
|
|
389
|
+
del self.expt_datas[obj.id]
|
|
390
|
+
if self._active_expt_data_id == obj.id:
|
|
391
|
+
self._active_expt_data_id = None
|
|
392
|
+
elif isinstance(obj, RawData):
|
|
393
|
+
if obj.id in self.raw_datas:
|
|
394
|
+
del self.raw_datas[obj.id]
|
|
395
|
+
if self._active_raw_data_id == obj.id:
|
|
396
|
+
self._active_raw_data_id = None
|
|
397
|
+
elif isinstance(obj, MCMCSim):
|
|
398
|
+
if obj.id in self.mcmcs:
|
|
399
|
+
del self.mcmcs[obj.id]
|
|
400
|
+
if self._active_mcmc_id == obj.id:
|
|
401
|
+
self._active_mcmc_id = None
|
|
402
|
+
else:
|
|
403
|
+
raise TypeError("Expected an instance of Model, FitResult, Simulation, ExptData, RawData, or MCMCSim.")
|
|
404
|
+
|
|
405
|
+
self.clean_up_dependencies_for_obj(obj)
|
|
406
|
+
|
|
407
|
+
def delete_object(self, obj: Model | FitResult | Simulation | ExptData | RawData | MCMCSim) -> None:
|
|
408
|
+
"""Compatibility wrapper; prefer typed delete methods."""
|
|
409
|
+
logger.warning("delete_object is a low-level API. Prefer typed delete methods.")
|
|
410
|
+
before = self._snapshot_object_ids()
|
|
411
|
+
self._delete_object_core(obj)
|
|
412
|
+
after = self._snapshot_object_ids()
|
|
413
|
+
self._emit_collection_events(before, after, notify_listeners=True)
|
|
414
|
+
self._finalize_active_context(reason="delete_object", emit_events=True)
|
|
415
|
+
|
|
416
|
+
def clean_up_dependencies_for_obj(self, obj: Model | FitResult | Simulation | ExptData | RawData | MCMCSim) -> None:
|
|
417
|
+
"""Clean up dependencies when an object is deleted."""
|
|
418
|
+
objs_to_delete = []
|
|
419
|
+
|
|
420
|
+
if isinstance(obj, MCMCSim):
|
|
421
|
+
pass # nothing to do
|
|
422
|
+
elif isinstance(obj, Simulation):
|
|
423
|
+
pass # nothing to do
|
|
424
|
+
elif isinstance(obj, FitResult):
|
|
425
|
+
pass # nothing to do
|
|
426
|
+
elif isinstance(obj, ExptData):
|
|
427
|
+
for fit in list(self.fits.values()):
|
|
428
|
+
if fit.expt_data_id == obj.id:
|
|
429
|
+
objs_to_delete.append(fit)
|
|
430
|
+
for mcmc in list(self.mcmcs.values()):
|
|
431
|
+
if mcmc.expt_data_id == obj.id:
|
|
432
|
+
objs_to_delete.append(mcmc)
|
|
433
|
+
elif isinstance(obj, RawData):
|
|
434
|
+
for expt_data in list(self.expt_datas.values()):
|
|
435
|
+
if expt_data.raw_data_id == obj.id:
|
|
436
|
+
objs_to_delete.append(expt_data)
|
|
437
|
+
elif isinstance(obj, Model):
|
|
438
|
+
for sim in list(self.simulations.values()):
|
|
439
|
+
if sim.model_id == obj.id:
|
|
440
|
+
objs_to_delete.append(sim)
|
|
441
|
+
for fit in list(self.fits.values()):
|
|
442
|
+
if fit.model_id == obj.id:
|
|
443
|
+
objs_to_delete.append(fit)
|
|
444
|
+
for expt_data in list(self.expt_datas.values()):
|
|
445
|
+
if expt_data.model_id == obj.id:
|
|
446
|
+
objs_to_delete.append(expt_data)
|
|
447
|
+
for mcmc in list(self.mcmcs.values()):
|
|
448
|
+
if mcmc.model_id == obj.id:
|
|
449
|
+
objs_to_delete.append(mcmc)
|
|
450
|
+
|
|
451
|
+
for o in objs_to_delete:
|
|
452
|
+
self._delete_object_core(o)
|
|
453
|
+
|
|
454
|
+
@property
|
|
455
|
+
def active_model(self) -> Model:
|
|
456
|
+
"""Get the currently active model."""
|
|
457
|
+
if self._active_model_id is None or self._active_model_id not in self.models:
|
|
458
|
+
raise IndexError("No active model set.")
|
|
459
|
+
return self.models[self._active_model_id]
|
|
460
|
+
|
|
461
|
+
@property
|
|
462
|
+
def active_model_id(self) -> uuid.UUID | None:
|
|
463
|
+
"""Get the ID of the currently active model."""
|
|
464
|
+
return self._active_model_id
|
|
465
|
+
|
|
466
|
+
@active_model_id.setter
|
|
467
|
+
def active_model_id(self, id: uuid.UUID | str | None) -> None:
|
|
468
|
+
"""Set the active model index and update the active model."""
|
|
469
|
+
if id is not None:
|
|
470
|
+
if isinstance(id,str):
|
|
471
|
+
id = uuid.UUID(id)
|
|
472
|
+
if not isinstance(id, uuid.UUID):
|
|
473
|
+
raise TypeError("Expected a UUID for active model ID.")
|
|
474
|
+
if id not in self.models:
|
|
475
|
+
raise ValueError(f"Model with ID {id} does not exist.")
|
|
476
|
+
self._active_model_id = id
|
|
477
|
+
self.update_ui_bindings(["model_name",])
|
|
478
|
+
# self.active_model = self.models[value] # Set the active model based on the index
|
|
479
|
+
# self.active_model_dict.update(self.active_model.to_dict()) # Update active_model with the new model's data
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
@property
|
|
483
|
+
def expt_data(self):
|
|
484
|
+
"""Get the experimental data."""
|
|
485
|
+
logger.warning("Deprecated L109 expt_data getter sm")
|
|
486
|
+
return self.active_expt_data
|
|
487
|
+
|
|
488
|
+
@expt_data.setter
|
|
489
|
+
def expt_data(self, value):
|
|
490
|
+
"""Set the experimental data."""
|
|
491
|
+
# if isinstance(value, ExptData):
|
|
492
|
+
# self._expt_data = value
|
|
493
|
+
logger.warning("Deprecated L117 expt_data setter sm")
|
|
494
|
+
raise NotImplementedError("Use add_expt_data instead")
|
|
495
|
+
# else:
|
|
496
|
+
# raise TypeError("Expected an instance of ExptData.")
|
|
497
|
+
|
|
498
|
+
@property
|
|
499
|
+
def active_sim_id(self) -> uuid.UUID|None:
|
|
500
|
+
return self._active_sim_id
|
|
501
|
+
|
|
502
|
+
@active_sim_id.setter
|
|
503
|
+
def active_sim_id(self, value: uuid.UUID | str | None ) -> None:
|
|
504
|
+
if value is not None:
|
|
505
|
+
if isinstance(value, str):
|
|
506
|
+
value = uuid.UUID(value)
|
|
507
|
+
if not isinstance(value, uuid.UUID):
|
|
508
|
+
raise TypeError("Expected a UUID for active simulation ID.")
|
|
509
|
+
if value not in self.simulations:
|
|
510
|
+
raise IndexError("Active simulation index does not exist in state.")
|
|
511
|
+
self._active_sim_id = value
|
|
512
|
+
|
|
513
|
+
@property
|
|
514
|
+
def active_simulation(self) -> Simulation|None:
|
|
515
|
+
"""Get the currently active simulation."""
|
|
516
|
+
return self.active_sim
|
|
517
|
+
|
|
518
|
+
@property
|
|
519
|
+
def active_sim(self) -> Simulation|None:
|
|
520
|
+
if self.active_sim_id is None:
|
|
521
|
+
return None
|
|
522
|
+
if self.active_sim_id in self.simulations:
|
|
523
|
+
return self.simulations[self.active_sim_id]
|
|
524
|
+
else:
|
|
525
|
+
raise ValueError("Active simulation ID not present in simulation list.")
|
|
526
|
+
|
|
527
|
+
@property
|
|
528
|
+
def active_expt_data_id(self) -> Optional[uuid.UUID]:
|
|
529
|
+
return self._active_expt_data_id
|
|
530
|
+
|
|
531
|
+
@active_expt_data_id.setter
|
|
532
|
+
def active_expt_data_id(self, value: uuid.UUID | str | None) -> None:
|
|
533
|
+
if value is not None:
|
|
534
|
+
if isinstance(value,str):
|
|
535
|
+
value = uuid.UUID(value)
|
|
536
|
+
if not isinstance(value,uuid.UUID):
|
|
537
|
+
raise TypeError("Expected a UUID for active expt_data ID.")
|
|
538
|
+
if value not in self.expt_datas:
|
|
539
|
+
raise IndexError("Active expt_data index does not exist in state.")
|
|
540
|
+
self._active_expt_data_id = value
|
|
541
|
+
|
|
542
|
+
@property
|
|
543
|
+
def active_expt_data(self) -> ExptData :
|
|
544
|
+
"""Get the currently active ExptData."""
|
|
545
|
+
if self._active_expt_data_id is None:
|
|
546
|
+
raise IndexError("No active ExptData set.")
|
|
547
|
+
# new_obj = ExptData()
|
|
548
|
+
# self.add_expt_data(new_obj)
|
|
549
|
+
# self.active_expt_data_id = new_obj.id # Set the active model to the newly created one
|
|
550
|
+
# return new_obj
|
|
551
|
+
else:
|
|
552
|
+
return(self.expt_datas[self._active_expt_data_id])
|
|
553
|
+
|
|
554
|
+
@property
|
|
555
|
+
def active_raw_data_id(self) -> Optional[uuid.UUID]:
|
|
556
|
+
return self._active_raw_data_id
|
|
557
|
+
|
|
558
|
+
@active_raw_data_id.setter
|
|
559
|
+
def active_raw_data_id(self, value: uuid.UUID | str | None) -> None:
|
|
560
|
+
if value is not None:
|
|
561
|
+
if isinstance(value,str):
|
|
562
|
+
value = uuid.UUID(value)
|
|
563
|
+
if not isinstance(value,uuid.UUID):
|
|
564
|
+
raise TypeError("Expected a UUID for active raw_data ID.")
|
|
565
|
+
if value not in self.raw_datas:
|
|
566
|
+
raise IndexError("Active raw_data index out of range")
|
|
567
|
+
self._active_raw_data_id =value
|
|
568
|
+
|
|
569
|
+
@property
|
|
570
|
+
def active_raw_data(self) -> Optional[RawData]:
|
|
571
|
+
if self.active_raw_data_id is None:
|
|
572
|
+
return None
|
|
573
|
+
if self.active_raw_data_id in self.raw_datas:
|
|
574
|
+
return self.raw_datas[self.active_raw_data_id]
|
|
575
|
+
else:
|
|
576
|
+
raise ValueError(f"Active raw_data id {self.active_raw_data_id} does not exist.")
|
|
577
|
+
|
|
578
|
+
@property
|
|
579
|
+
def active_fit_id(self) -> Optional[uuid.UUID]:
|
|
580
|
+
if self._active_fit_id is None:
|
|
581
|
+
return None
|
|
582
|
+
else:
|
|
583
|
+
return self._active_fit_id
|
|
584
|
+
|
|
585
|
+
@active_fit_id.setter
|
|
586
|
+
def active_fit_id(self, value: uuid.UUID | str | None) -> None:
|
|
587
|
+
if value is not None:
|
|
588
|
+
if isinstance(value,str):
|
|
589
|
+
value=uuid.UUID(value)
|
|
590
|
+
if not isinstance(value,uuid.UUID):
|
|
591
|
+
raise TypeError("Expected a UUID for active fit ID.")
|
|
592
|
+
if value not in self.fits:
|
|
593
|
+
raise IndexError("Active fit index out of range.")
|
|
594
|
+
self._active_fit_id = value
|
|
595
|
+
|
|
596
|
+
@property
|
|
597
|
+
def active_fit(self) -> FitResult:
|
|
598
|
+
"""Get the currently active fit."""
|
|
599
|
+
if self.active_fit_id is None:
|
|
600
|
+
raise IndexError("No active fit set.")
|
|
601
|
+
elif self.active_fit_id in self.fits:
|
|
602
|
+
return self.fits[self.active_fit_id]
|
|
603
|
+
else:
|
|
604
|
+
raise(IndexError("Active fit ID not present in fit list."))
|
|
605
|
+
|
|
606
|
+
@property
|
|
607
|
+
def active_mcmc_id(self) -> Optional[uuid.UUID]:
|
|
608
|
+
return self._active_mcmc_id
|
|
609
|
+
|
|
610
|
+
@active_mcmc_id.setter
|
|
611
|
+
def active_mcmc_id(self, value: uuid.UUID | str | None) -> None:
|
|
612
|
+
if value is not None:
|
|
613
|
+
if isinstance(value,str):
|
|
614
|
+
value=uuid.UUID(value)
|
|
615
|
+
if not isinstance(value,uuid.UUID):
|
|
616
|
+
raise TypeError("Expected a UUID for active mcmc ID.")
|
|
617
|
+
if value not in self.mcmcs:
|
|
618
|
+
raise IndexError("Active mcmc index out of range.")
|
|
619
|
+
self._active_mcmc_id = value
|
|
620
|
+
|
|
621
|
+
@property
|
|
622
|
+
def active_mcmc(self) -> MCMCSim:
|
|
623
|
+
"""Get the currently active mcmc."""
|
|
624
|
+
if self.active_mcmc_id is None:
|
|
625
|
+
raise IndexError("No active mcmc set.")
|
|
626
|
+
elif self.active_mcmc_id in self.mcmcs:
|
|
627
|
+
return self.mcmcs[self.active_mcmc_id]
|
|
628
|
+
else:
|
|
629
|
+
raise(IndexError("Active mcmc ID not present in mcmc list."))
|
|
630
|
+
|
|
631
|
+
@property
|
|
632
|
+
def active_sim_or_none(self) -> Simulation | None:
|
|
633
|
+
if self.active_sim_id is None:
|
|
634
|
+
return None
|
|
635
|
+
try:
|
|
636
|
+
return self.active_sim
|
|
637
|
+
except Exception:
|
|
638
|
+
return None
|
|
639
|
+
|
|
640
|
+
@property
|
|
641
|
+
def active_expt_data_or_none(self) -> ExptData | None:
|
|
642
|
+
if self.active_expt_data_id is None:
|
|
643
|
+
return None
|
|
644
|
+
try:
|
|
645
|
+
return self.active_expt_data
|
|
646
|
+
except Exception:
|
|
647
|
+
return None
|
|
648
|
+
|
|
649
|
+
@property
|
|
650
|
+
def active_raw_data_or_none(self) -> RawData | None:
|
|
651
|
+
if self.active_raw_data_id is None:
|
|
652
|
+
return None
|
|
653
|
+
try:
|
|
654
|
+
return self.active_raw_data
|
|
655
|
+
except Exception:
|
|
656
|
+
return None
|
|
657
|
+
|
|
658
|
+
@property
|
|
659
|
+
def active_fit_or_none(self) -> FitResult | None:
|
|
660
|
+
if self.active_fit_id is None:
|
|
661
|
+
return None
|
|
662
|
+
try:
|
|
663
|
+
return self.active_fit
|
|
664
|
+
except Exception:
|
|
665
|
+
return None
|
|
666
|
+
|
|
667
|
+
@property
|
|
668
|
+
def active_mcmc_or_none(self) -> MCMCSim | None:
|
|
669
|
+
if self.active_mcmc_id is None:
|
|
670
|
+
return None
|
|
671
|
+
try:
|
|
672
|
+
return self.active_mcmc
|
|
673
|
+
except Exception:
|
|
674
|
+
return None
|
|
675
|
+
|
|
676
|
+
@property
|
|
677
|
+
def model_name(self):
|
|
678
|
+
return self.active_model.name if isinstance(self.active_model, Model) else ""
|
|
679
|
+
|
|
680
|
+
@model_name.setter
|
|
681
|
+
def model_name(self, value):
|
|
682
|
+
if self.active_model is not None:
|
|
683
|
+
self.active_model.name = value
|
|
684
|
+
self._model_name = value
|
|
685
|
+
else:
|
|
686
|
+
self._model_name = value
|
|
687
|
+
|
|
688
|
+
@property
|
|
689
|
+
def eq_mat(self):
|
|
690
|
+
return (
|
|
691
|
+
self.active_model.eq_mat
|
|
692
|
+
if isinstance(self.active_model, Model)
|
|
693
|
+
else self._eq_mat
|
|
694
|
+
)
|
|
695
|
+
|
|
696
|
+
@eq_mat.setter
|
|
697
|
+
def eq_mat(self, value):
|
|
698
|
+
if self.active_model is not None:
|
|
699
|
+
self.active_model.eq_mat = value
|
|
700
|
+
self._eq_mat = value
|
|
701
|
+
else:
|
|
702
|
+
self._eq_mat = value
|
|
703
|
+
|
|
704
|
+
@property
|
|
705
|
+
def nComp(self):
|
|
706
|
+
return (
|
|
707
|
+
self.active_model.nComp
|
|
708
|
+
if isinstance(self.active_model, Model)
|
|
709
|
+
else self._nComp
|
|
710
|
+
)
|
|
711
|
+
|
|
712
|
+
@nComp.setter
|
|
713
|
+
def nComp(self, value):
|
|
714
|
+
if self.active_model is not None:
|
|
715
|
+
self.active_model.nComp = value
|
|
716
|
+
self._nComp = value
|
|
717
|
+
else:
|
|
718
|
+
self._nComp = value
|
|
719
|
+
|
|
720
|
+
@property
|
|
721
|
+
def nStep(self):
|
|
722
|
+
return (
|
|
723
|
+
self.active_model.nStep
|
|
724
|
+
if isinstance(self.active_model, Model)
|
|
725
|
+
else self._nStep
|
|
726
|
+
)
|
|
727
|
+
|
|
728
|
+
@nStep.setter
|
|
729
|
+
def nStep(self, value):
|
|
730
|
+
if self.active_model is not None:
|
|
731
|
+
self.active_model.nStep = value
|
|
732
|
+
self._nStep = value
|
|
733
|
+
else:
|
|
734
|
+
self._nStep = value
|
|
735
|
+
|
|
736
|
+
@property
|
|
737
|
+
def eq_str(self):
|
|
738
|
+
return (
|
|
739
|
+
self.active_model.eq_str
|
|
740
|
+
if isinstance(self.active_model, Model)
|
|
741
|
+
else self._eq_str
|
|
742
|
+
)
|
|
743
|
+
|
|
744
|
+
@eq_str.setter
|
|
745
|
+
def eq_str(self, value):
|
|
746
|
+
if self.active_model is not None:
|
|
747
|
+
self.active_model.eq_str = value
|
|
748
|
+
self._eq_str = value
|
|
749
|
+
else:
|
|
750
|
+
self._eq_str = value
|
|
751
|
+
|
|
752
|
+
@property
|
|
753
|
+
def eq_mat_str(self):
|
|
754
|
+
return (
|
|
755
|
+
self.active_model.eq_mat_str
|
|
756
|
+
if isinstance(self.active_model, Model)
|
|
757
|
+
else self._eq_mat_str
|
|
758
|
+
)
|
|
759
|
+
|
|
760
|
+
@eq_mat_str.setter
|
|
761
|
+
def eq_mat_str(self, value):
|
|
762
|
+
if self.active_model is not None:
|
|
763
|
+
self.active_model.eq_mat_str = value
|
|
764
|
+
self._eq_mat_str = value
|
|
765
|
+
else:
|
|
766
|
+
self._eq_mat_str = value
|
|
767
|
+
|
|
768
|
+
@property
|
|
769
|
+
def components(self):
|
|
770
|
+
return (
|
|
771
|
+
self.active_model.components
|
|
772
|
+
if isinstance(self.active_model, Model)
|
|
773
|
+
else self._components
|
|
774
|
+
)
|
|
775
|
+
|
|
776
|
+
@components.setter
|
|
777
|
+
def components(self, value):
|
|
778
|
+
if self.active_model is not None:
|
|
779
|
+
self.active_model.components = value
|
|
780
|
+
self._components = value
|
|
781
|
+
else:
|
|
782
|
+
self._components = value
|
|
783
|
+
|
|
784
|
+
@property
|
|
785
|
+
def species(self):
|
|
786
|
+
return (
|
|
787
|
+
self.active_model.species
|
|
788
|
+
if isinstance(self.active_model, Model)
|
|
789
|
+
else self._species
|
|
790
|
+
)
|
|
791
|
+
|
|
792
|
+
@species.setter
|
|
793
|
+
def species(self, value):
|
|
794
|
+
if self.active_model is not None:
|
|
795
|
+
self.active_model.species = value
|
|
796
|
+
self._species = value
|
|
797
|
+
else:
|
|
798
|
+
self._species = value
|
|
799
|
+
|
|
800
|
+
@property
|
|
801
|
+
def comp_concs(self):
|
|
802
|
+
return (
|
|
803
|
+
self.active_model.component_concs
|
|
804
|
+
if isinstance(self.active_model, Model)
|
|
805
|
+
else self._comp_concs
|
|
806
|
+
)
|
|
807
|
+
|
|
808
|
+
@comp_concs.setter
|
|
809
|
+
def comp_concs(self, value):
|
|
810
|
+
if self.active_model is not None:
|
|
811
|
+
self.active_model.component_concs = value
|
|
812
|
+
self._comp_concs = value
|
|
813
|
+
else:
|
|
814
|
+
self._comp_concs = value
|
|
815
|
+
|
|
816
|
+
@property
|
|
817
|
+
def binding_constants(self):
|
|
818
|
+
return (
|
|
819
|
+
self.active_model.binding_constants
|
|
820
|
+
if isinstance(self.active_model, Model)
|
|
821
|
+
else self._binding_constants
|
|
822
|
+
)
|
|
823
|
+
|
|
824
|
+
@binding_constants.setter
|
|
825
|
+
def binding_constants(self, value):
|
|
826
|
+
if self.active_model is not None:
|
|
827
|
+
self.active_model.binding_constants = value
|
|
828
|
+
self._binding_constants = value
|
|
829
|
+
else:
|
|
830
|
+
self._binding_constants = value
|
|
831
|
+
|
|
832
|
+
@property
|
|
833
|
+
def component_names(self):
|
|
834
|
+
return (
|
|
835
|
+
self.active_model.component_names
|
|
836
|
+
if isinstance(self.active_model, Model)
|
|
837
|
+
else self._component_names
|
|
838
|
+
)
|
|
839
|
+
|
|
840
|
+
@component_names.setter
|
|
841
|
+
def component_names(self, value):
|
|
842
|
+
if self.active_model is not None:
|
|
843
|
+
self.active_model.component_names = value
|
|
844
|
+
self._component_names = value
|
|
845
|
+
else:
|
|
846
|
+
self._component_names = value
|
|
847
|
+
|
|
848
|
+
|
|
849
|
+
def add_model(self, model: Model, emit_events: bool = True) -> None:
|
|
850
|
+
"""Add a model to the state manager."""
|
|
851
|
+
if not isinstance(model, Model):
|
|
852
|
+
raise TypeError("Expected a Model instance.")
|
|
853
|
+
self.models[model.id] = model # Use UUID as key
|
|
854
|
+
self.active_model_id = model.id
|
|
855
|
+
self._finalize_active_context(reason="add_model", emit_events=emit_events)
|
|
856
|
+
|
|
857
|
+
def add_fit(self, fit: FitResult, emit_events: bool = True) -> None:
|
|
858
|
+
"""Add a fit result to the state manager."""
|
|
859
|
+
if not isinstance(fit, FitResult):
|
|
860
|
+
raise TypeError("Expected a FitResult instance.")
|
|
861
|
+
self.fits[fit.id] = fit
|
|
862
|
+
self.active_fit_id = fit.id # Set the newly added fit as active
|
|
863
|
+
self._finalize_active_context(reason="add_fit", emit_events=emit_events)
|
|
864
|
+
|
|
865
|
+
def add_sim(self, sim: Simulation, emit_events: bool = True) -> None:
|
|
866
|
+
"""Add a simulation to the state manager."""
|
|
867
|
+
if not isinstance(sim, Simulation):
|
|
868
|
+
raise TypeError("Expected a Simulation instance.")
|
|
869
|
+
self.simulations[sim.id]=sim
|
|
870
|
+
self.active_sim_id = sim.id
|
|
871
|
+
self._finalize_active_context(reason="add_sim", emit_events=emit_events)
|
|
872
|
+
|
|
873
|
+
def add_expt_data(self, expt_data: ExptData, emit_events: bool = True) -> None:
|
|
874
|
+
"""Add experimental data to the state manager."""
|
|
875
|
+
if not isinstance(expt_data, ExptData):
|
|
876
|
+
raise TypeError("Expected an ExptData instance.")
|
|
877
|
+
self.expt_datas[expt_data.id]=expt_data
|
|
878
|
+
self.active_expt_data_id = expt_data.id
|
|
879
|
+
self.active_expt_data.col_details = {k: self.active_expt_data.col_details[k] if k in self.active_expt_data.col_details else {'depindep': None} for k in self.active_expt_data.data.columns}
|
|
880
|
+
self._finalize_active_context(reason="add_expt_data", emit_events=emit_events)
|
|
881
|
+
|
|
882
|
+
|
|
883
|
+
def add_raw_data(self, raw_data: RawData, emit_events: bool = True) -> None:
|
|
884
|
+
"""Add raw data to the state manager."""
|
|
885
|
+
if not isinstance(raw_data, RawData):
|
|
886
|
+
raise TypeError("Expected a RawData instance.")
|
|
887
|
+
if raw_data.id in self.raw_datas:
|
|
888
|
+
raise ValueError(f"RawData with id {raw_data.id} already exists.")
|
|
889
|
+
self.raw_datas[raw_data.id] = raw_data
|
|
890
|
+
self.active_raw_data_id = raw_data.id
|
|
891
|
+
self._finalize_active_context(reason="add_raw_data", emit_events=emit_events)
|
|
892
|
+
|
|
893
|
+
def add_mcmc(self, mcmc: MCMCSim, emit_events: bool = True) -> None:
|
|
894
|
+
"""Add a MCMC result to the state manager."""
|
|
895
|
+
if not isinstance(mcmc, MCMCSim):
|
|
896
|
+
raise TypeError("Expected an MCMCSim instance.")
|
|
897
|
+
self.mcmcs[mcmc.id] = mcmc
|
|
898
|
+
self.active_mcmc_id = mcmc.id
|
|
899
|
+
self._finalize_active_context(reason="add_mcmc", emit_events=emit_events)
|
|
900
|
+
|
|
901
|
+
def add_expt_data_type(self, expt_data_type: ExptDataType) -> None:
|
|
902
|
+
"""Add an experimental data type to the state manager."""
|
|
903
|
+
if not isinstance(expt_data_type, ExptDataType):
|
|
904
|
+
raise TypeError("Expected an ExptDataType instance.")
|
|
905
|
+
if expt_data_type.name.lower() in self._expt_dtypes:
|
|
906
|
+
raise ValueError(f"ExptDataType with name {expt_data_type.name} already exists.")
|
|
907
|
+
self._expt_dtypes[expt_data_type.name] = expt_data_type
|
|
908
|
+
|
|
909
|
+
def _init_bindables(self):
|
|
910
|
+
# """Initialize bindable attributes."""
|
|
911
|
+
|
|
912
|
+
self._nComp: int = 2
|
|
913
|
+
self._nComp: int = 2 # Number of components
|
|
914
|
+
self._nStep: int = 20
|
|
915
|
+
self._eq_str = ""
|
|
916
|
+
self._eq_consts = None # Optional attribute for equilibrium constants
|
|
917
|
+
self._model_name = ""
|
|
918
|
+
self._comp_concs = pd.DataFrame({})
|
|
919
|
+
self._components = []
|
|
920
|
+
self._eq_mat = np.array([]) # Initialize eqMat as an empty array
|
|
921
|
+
self._binding_constants = []
|
|
922
|
+
self._species = []
|
|
923
|
+
self._component_names = []
|
|
924
|
+
self._eq_mat_str = ""
|
|
925
|
+
|
|
926
|
+
def new_model(self, name="New model") -> uuid.UUID:
|
|
927
|
+
"""Create a new model and set it as the active model."""
|
|
928
|
+
existing_names = [model.name for model in self.models.values()]
|
|
929
|
+
if name in existing_names:
|
|
930
|
+
n_models_with_name = len([
|
|
931
|
+
model for model in self.models.values() if model.name.startswith(name)])
|
|
932
|
+
name = f"{name} ({n_models_with_name})" # Append a number to the name if it already exists
|
|
933
|
+
|
|
934
|
+
new_model = Model(name=name,
|
|
935
|
+
nComp=self.nComp,
|
|
936
|
+
nStep=self.nStep,
|
|
937
|
+
component_concs=self.comp_concs.copy(),
|
|
938
|
+
component_names=self.component_names.copy(),
|
|
939
|
+
components=self.components.copy(),
|
|
940
|
+
)
|
|
941
|
+
|
|
942
|
+
self.add_model(new_model)
|
|
943
|
+
|
|
944
|
+
return new_model.id
|
|
945
|
+
|
|
946
|
+
def _default_models_path(self) -> str:
|
|
947
|
+
if getattr(sys, 'frozen', False):
|
|
948
|
+
# PyInstaller onefile: data files are extracted to sys._MEIPASS
|
|
949
|
+
return os.path.join(getattr(sys, '_MEIPASS'), 'webgui', 'default_models.json')
|
|
950
|
+
return os.path.join(os.path.dirname(__file__), '..', 'default_models.json')
|
|
951
|
+
|
|
952
|
+
def _load_default_models_data(self) -> dict[str, Any]:
|
|
953
|
+
with open(self._default_models_path(), 'r') as f:
|
|
954
|
+
return json.load(f)
|
|
955
|
+
|
|
956
|
+
def _default_model_ids_from_bundle(self) -> set[uuid.UUID]:
|
|
957
|
+
ids: set[uuid.UUID] = set()
|
|
958
|
+
try:
|
|
959
|
+
default_models_data = self._load_default_models_data()
|
|
960
|
+
except Exception as e:
|
|
961
|
+
logger.warning(f"Could not load bundled default model IDs: {e}")
|
|
962
|
+
return ids
|
|
963
|
+
for model_data in default_models_data.get("models", []):
|
|
964
|
+
mid = self._normalize_uuid(model_data.get("id"))
|
|
965
|
+
if mid is not None:
|
|
966
|
+
ids.add(mid)
|
|
967
|
+
return ids
|
|
968
|
+
|
|
969
|
+
def _initialize_default_models(self, emit_events: bool = True):
|
|
970
|
+
"""Initialize with default models."""
|
|
971
|
+
self.default_model_ids = []
|
|
972
|
+
try:
|
|
973
|
+
default_models_data = self._load_default_models_data()
|
|
974
|
+
except Exception as e:
|
|
975
|
+
logger.error(f"Error loading default models: {e}")
|
|
976
|
+
raise RuntimeError("Unable to initialize built-in models.") from e
|
|
977
|
+
|
|
978
|
+
# Create models from the loaded data
|
|
979
|
+
for model_data in default_models_data.get('models', []):
|
|
980
|
+
model = Model(**model_data)
|
|
981
|
+
# Reconstruct complex objects
|
|
982
|
+
model.components = [Component(**comp) for comp in model_data.get("components", [])]
|
|
983
|
+
model.binding_constants = [BindingConstant(**k) for k in model_data.get("binding_constants", [])]
|
|
984
|
+
model.eq_mat = np.array(model_data.get("eq_mat", []))
|
|
985
|
+
model.component_concs = pd.DataFrame(model_data.get("component_concs", {}))
|
|
986
|
+
self.add_model(model, emit_events=emit_events)
|
|
987
|
+
self.default_model_ids.append(model.id)
|
|
988
|
+
|
|
989
|
+
if len(self.models) == 0:
|
|
990
|
+
raise RuntimeError("No built-in models were loaded.")
|
|
991
|
+
|
|
992
|
+
logger.info(f"Loaded {len(self.default_model_ids)} default models")
|
|
993
|
+
|
|
994
|
+
# def _set_initial_simFig_style(self):
|
|
995
|
+
# """Set the initial style for the simulation figure."""
|
|
996
|
+
# self.simFig_data = {
|
|
997
|
+
# 'data': [],
|
|
998
|
+
# 'layout': {
|
|
999
|
+
# 'margin': {'l':50, 'r':0, 'b':20, 't':0},
|
|
1000
|
+
# 'plot_bgcolor': '#E5ECF6',
|
|
1001
|
+
# 'legend': {'y': .95},
|
|
1002
|
+
# 'xaxis': {'title': 'x-axis'},
|
|
1003
|
+
# 'yaxis': {'title': 'y-axis'},
|
|
1004
|
+
# },
|
|
1005
|
+
# }
|
|
1006
|
+
|
|
1007
|
+
async def new_project(self):
|
|
1008
|
+
"""Create a new project."""
|
|
1009
|
+
try:
|
|
1010
|
+
with ui.dialog() as dialog, ui.card():
|
|
1011
|
+
ui.label("Create new project? Unsaved changes will be lost.")
|
|
1012
|
+
with ui.row():
|
|
1013
|
+
ui.button("Yes", on_click=lambda: dialog.submit(True))
|
|
1014
|
+
ui.button("No", on_click=lambda: dialog.submit(False))
|
|
1015
|
+
|
|
1016
|
+
result = await dialog
|
|
1017
|
+
if result:
|
|
1018
|
+
# Reset the UIState and reload the page
|
|
1019
|
+
# self.sd = UIState() # Reset the SimData instance
|
|
1020
|
+
app.storage.user["state-data_old"] = app.storage.user.get("state-data", "")
|
|
1021
|
+
self.__init__(load_prior_state=False) # Reinitialize the StateManager
|
|
1022
|
+
|
|
1023
|
+
# self.simFig_data["data"] = [] # Clear the simulation figure data
|
|
1024
|
+
# self.simFig.update()
|
|
1025
|
+
app.storage.user["state-data"] = self.to_json()
|
|
1026
|
+
ui.navigate.reload()
|
|
1027
|
+
else:
|
|
1028
|
+
ui.notify("New project creation cancelled", type="info")
|
|
1029
|
+
except Exception as e:
|
|
1030
|
+
logger.error(f"Error creating new project: {str(e)}")
|
|
1031
|
+
ui.notify("Failed to create new project", type="negative")
|
|
1032
|
+
|
|
1033
|
+
async def open_project(self):
|
|
1034
|
+
"""Open an existing project."""
|
|
1035
|
+
try:
|
|
1036
|
+
|
|
1037
|
+
with ui.dialog() as dialog, ui.card():
|
|
1038
|
+
ui.label("Open Project File")
|
|
1039
|
+
upload_box = ui.upload(label="Choose file", auto_upload=True).props(
|
|
1040
|
+
'accept=".json, .json.gz"'
|
|
1041
|
+
)
|
|
1042
|
+
ui.button("Cancel", on_click=lambda: dialog.submit("cancel"))
|
|
1043
|
+
|
|
1044
|
+
def on_upload_complete(e: UploadEventArguments) -> None:
|
|
1045
|
+
dialog.submit(e) # Store result for later
|
|
1046
|
+
|
|
1047
|
+
upload_box.on_upload(on_upload_complete)
|
|
1048
|
+
|
|
1049
|
+
result: UploadEventArguments|str= await dialog
|
|
1050
|
+
|
|
1051
|
+
if isinstance(result, str) and result == "cancel":
|
|
1052
|
+
ui.notify("Project loading cancelled", type="info")
|
|
1053
|
+
return
|
|
1054
|
+
elif isinstance(result, UploadEventArguments):
|
|
1055
|
+
filename = result.file.name
|
|
1056
|
+
|
|
1057
|
+
if filename.endswith(".gz"):
|
|
1058
|
+
file_content = await result.file.read()
|
|
1059
|
+
with gzip.GzipFile(fileobj=io.BytesIO(file_content), mode="rb") as gz:
|
|
1060
|
+
file_content = gz.read().decode("utf-8")
|
|
1061
|
+
else:
|
|
1062
|
+
file_content = await result.file.text("utf-8")
|
|
1063
|
+
|
|
1064
|
+
app.storage.user["state-data"] = file_content
|
|
1065
|
+
try:
|
|
1066
|
+
self.load_storage_into_state()
|
|
1067
|
+
except Exception as e:
|
|
1068
|
+
logger.error(f"Error loading model data: {e}")
|
|
1069
|
+
#ui.navigate.reload()
|
|
1070
|
+
ui.notify("Project loaded successfully", type="info")
|
|
1071
|
+
else:
|
|
1072
|
+
raise RuntimeError("Unexpected result type from dialog submission.")
|
|
1073
|
+
|
|
1074
|
+
except Exception as e:
|
|
1075
|
+
logger.error(f"Error opening project: {str(e)}")
|
|
1076
|
+
ui.notify("Failed to open project", type="negative")
|
|
1077
|
+
|
|
1078
|
+
def save_to_storage(self, e=None):
|
|
1079
|
+
"""Save the current state to user storage."""
|
|
1080
|
+
app.storage.user["state-data"] = self.to_json()
|
|
1081
|
+
|
|
1082
|
+
async def save_project(self):
|
|
1083
|
+
"""Save the current project."""
|
|
1084
|
+
logger.info("saving...")
|
|
1085
|
+
self.save_to_storage() # Save the current state to user storage
|
|
1086
|
+
|
|
1087
|
+
timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
|
|
1088
|
+
|
|
1089
|
+
buffer = io.BytesIO()
|
|
1090
|
+
|
|
1091
|
+
with gzip.GzipFile(fileobj=buffer, mode="wb") as gz:
|
|
1092
|
+
gz.write(app.storage.user["state-data"].encode("utf-8"))
|
|
1093
|
+
|
|
1094
|
+
# Ensure data is flushed and gzip stream is closed before reading
|
|
1095
|
+
buffer.seek(0)
|
|
1096
|
+
filename = f"bindtools_project_{timestamp}.json.gz"
|
|
1097
|
+
ui.download.content(buffer.read(), filename=filename)
|
|
1098
|
+
ui.notify(f"Project saved as {filename}", type="info")
|
|
1099
|
+
|
|
1100
|
+
def to_json(self):
|
|
1101
|
+
return json.dumps(self.to_dict())
|
|
1102
|
+
|
|
1103
|
+
def to_dict(self):
|
|
1104
|
+
"""Convert the state to a dictionary."""
|
|
1105
|
+
return {
|
|
1106
|
+
#"simFig_data": self.simFig_data if hasattr(self, "simFig_data") else {},
|
|
1107
|
+
#"simFig": self.simFig.to_dict() if hasattr(self, "simFig") else {},
|
|
1108
|
+
"nComp": int(self.nComp),
|
|
1109
|
+
"nStep": int(self.nStep),
|
|
1110
|
+
"eq_str": self.eq_str,
|
|
1111
|
+
"eq_mat": (
|
|
1112
|
+
self.eq_mat.tolist() if isinstance(self.eq_mat, np.ndarray) else []
|
|
1113
|
+
), # Convert numpy arrays to lists
|
|
1114
|
+
"eq_mat_str": (
|
|
1115
|
+
self.eq_mat_str if hasattr(self, "eq_mat_str") else ""
|
|
1116
|
+
), # Optional attribute
|
|
1117
|
+
"components": (
|
|
1118
|
+
[asdict(comp) for comp in self.components]
|
|
1119
|
+
if hasattr(self, "components")
|
|
1120
|
+
else []
|
|
1121
|
+
), # Convert Component objects to dicts
|
|
1122
|
+
"species": (
|
|
1123
|
+
self.species if hasattr(self, "species") else []
|
|
1124
|
+
), # Optional attribute
|
|
1125
|
+
# "eq_consts": (
|
|
1126
|
+
# self.eq_consts if hasattr(self, "eq_consts") else None
|
|
1127
|
+
# ), # Optional attribute
|
|
1128
|
+
"model_name": self.model_name,
|
|
1129
|
+
"comp_concs": self.comp_concs.to_dict(orient="list"),
|
|
1130
|
+
"binding_constants": (
|
|
1131
|
+
[asdict(k) for k in self.binding_constants]
|
|
1132
|
+
if hasattr(self, "binding_constants")
|
|
1133
|
+
else []
|
|
1134
|
+
),
|
|
1135
|
+
"component_names": (
|
|
1136
|
+
self.component_names if hasattr(self, "component_names") else []
|
|
1137
|
+
), # Optional attribute
|
|
1138
|
+
"models": (
|
|
1139
|
+
[model.to_dict() for model in self.models.values()]
|
|
1140
|
+
if hasattr(self, "models")
|
|
1141
|
+
else []
|
|
1142
|
+
), # Optional attribute
|
|
1143
|
+
"simulations": (
|
|
1144
|
+
[sim.to_dict() for sim in self.simulations.values()]
|
|
1145
|
+
if hasattr(self, "simulations")
|
|
1146
|
+
else []
|
|
1147
|
+
), # List of simulations
|
|
1148
|
+
"raw_datas": (
|
|
1149
|
+
[raw_data.to_dict() for raw_data in self.raw_datas.values()]
|
|
1150
|
+
if hasattr(self, "raw_datas")
|
|
1151
|
+
else []
|
|
1152
|
+
), # List of raw data
|
|
1153
|
+
"expt_datas": (
|
|
1154
|
+
[expt_data.to_dict() for expt_data in self.expt_datas.values()]
|
|
1155
|
+
if hasattr(self, "expt_datas")
|
|
1156
|
+
else []
|
|
1157
|
+
), # List of experimental data
|
|
1158
|
+
"_expt_dtypes": (
|
|
1159
|
+
{name: asdict(dtype) for name, dtype in self._expt_dtypes.items()}
|
|
1160
|
+
if hasattr(self, "_expt_dtypes")
|
|
1161
|
+
else {}
|
|
1162
|
+
), # Dictionary of experimental data types
|
|
1163
|
+
"fits": (
|
|
1164
|
+
[fit.to_dict() for fit in self.fits.values()]
|
|
1165
|
+
if hasattr(self, "fits")
|
|
1166
|
+
else []
|
|
1167
|
+
), # List of fits
|
|
1168
|
+
"mcmcs": (
|
|
1169
|
+
[
|
|
1170
|
+
{
|
|
1171
|
+
"id": str(m.id),
|
|
1172
|
+
"nwalkers": m.nwalkers,
|
|
1173
|
+
"nsteps_target": m.nsteps_target,
|
|
1174
|
+
"burn": m.burn,
|
|
1175
|
+
"thin": m.thin,
|
|
1176
|
+
"seed": m.seed,
|
|
1177
|
+
"chains": m.chains.tolist() if isinstance(m.chains, np.ndarray) else [],
|
|
1178
|
+
"priors": m.priors,
|
|
1179
|
+
"model_id": str(m.model_id) if m.model_id else None,
|
|
1180
|
+
"expt_data_id": str(m.expt_data_id) if m.expt_data_id else None,
|
|
1181
|
+
"nsteps_done": m.nsteps_done,
|
|
1182
|
+
}
|
|
1183
|
+
for m in self.mcmcs.values()
|
|
1184
|
+
]
|
|
1185
|
+
if hasattr(self, "mcmcs")
|
|
1186
|
+
else []
|
|
1187
|
+
),
|
|
1188
|
+
"active_model_id": (
|
|
1189
|
+
str(self.active_model_id) if hasattr(self, "_active_model_id") else None
|
|
1190
|
+
), # Index of the active model
|
|
1191
|
+
"active_sim_id": (
|
|
1192
|
+
str(self.active_sim_id) if hasattr(self, "_active_sim_id") else None
|
|
1193
|
+
), # Index of the active simulation
|
|
1194
|
+
"active_expt_data_id": (
|
|
1195
|
+
str(self.active_expt_data_id) if hasattr(self, "_active_expt_data_id") else None
|
|
1196
|
+
), # Index of the active experimental data
|
|
1197
|
+
"active_fit_id": (
|
|
1198
|
+
str(self.active_fit_id) if hasattr(self, "_active_fit_id") else None
|
|
1199
|
+
), # Index of the active fit
|
|
1200
|
+
"active_raw_data_id": (
|
|
1201
|
+
str(self.active_raw_data_id) if hasattr(self, "_active_raw_data_id") else None
|
|
1202
|
+
),
|
|
1203
|
+
"active_mcmc_id": (
|
|
1204
|
+
str(self.active_mcmc_id) if hasattr(self, "_active_mcmc_id") else None
|
|
1205
|
+
),
|
|
1206
|
+
"default_model_ids": (
|
|
1207
|
+
[str(m) for m in self.default_model_ids]
|
|
1208
|
+
if hasattr(self,"default_model_ids")
|
|
1209
|
+
else
|
|
1210
|
+
[]
|
|
1211
|
+
)
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
def from_json(self, json_str: str) -> None:
|
|
1215
|
+
"""Load the state from a JSON string."""
|
|
1216
|
+
data = json.loads(json_str)
|
|
1217
|
+
mtemp = data.get("models", [])
|
|
1218
|
+
self.models = {}
|
|
1219
|
+
if mtemp:
|
|
1220
|
+
for model in mtemp:
|
|
1221
|
+
self.add_model(Model(**model), emit_events=False)
|
|
1222
|
+
m_id = uuid.UUID(model['id'])
|
|
1223
|
+
self.models[m_id].components = [
|
|
1224
|
+
Component(**comp) for comp in model.get("components", [])
|
|
1225
|
+
]
|
|
1226
|
+
self.models[m_id].binding_constants = [
|
|
1227
|
+
BindingConstant(**k) for k in model.get("binding_constants", [])
|
|
1228
|
+
]
|
|
1229
|
+
self.models[m_id].eq_mat = np.array(
|
|
1230
|
+
model.get("eq_mat", [])
|
|
1231
|
+
) # Convert numpy arrays to lists
|
|
1232
|
+
self.models[m_id].component_concs = pd.DataFrame(
|
|
1233
|
+
model.get("component_concs", {})
|
|
1234
|
+
)
|
|
1235
|
+
|
|
1236
|
+
|
|
1237
|
+
self.simulations = {}
|
|
1238
|
+
simtemp = data.get("simulations", [])
|
|
1239
|
+
if simtemp:
|
|
1240
|
+
for sim in simtemp:
|
|
1241
|
+
simulation = Simulation(**sim)
|
|
1242
|
+
simulation.model_id = uuid.UUID(sim.get("model_id"))
|
|
1243
|
+
simulation.comp_concs = pd.DataFrame(sim.get("comp_concs", {}))
|
|
1244
|
+
# simulation.model = Model(**sim.get("model", {}))
|
|
1245
|
+
# simulation.params = [BindingConstant(**k) for k in sim.get('params', [])]
|
|
1246
|
+
simulation.results = pd.DataFrame(sim.get("results", {}))
|
|
1247
|
+
self.simulations[simulation.id]=simulation
|
|
1248
|
+
|
|
1249
|
+
self.fits = {}
|
|
1250
|
+
fittemp = data.get("fits", []) # List of fits
|
|
1251
|
+
if fittemp:
|
|
1252
|
+
for fit in fittemp:
|
|
1253
|
+
fit_result = FitResult(**fit)
|
|
1254
|
+
fit_result.fit_speciation = pd.DataFrame(
|
|
1255
|
+
fit.get("fit_speciation", {}))
|
|
1256
|
+
fit_result.calc_obs = pd.DataFrame(
|
|
1257
|
+
fit.get("calc_obs", {}))
|
|
1258
|
+
self.fits[fit_result.id]=fit_result
|
|
1259
|
+
|
|
1260
|
+
self.mcmcs = {}
|
|
1261
|
+
mcmc_temp = data.get("mcmcs", [])
|
|
1262
|
+
if mcmc_temp:
|
|
1263
|
+
for mcmc in mcmc_temp:
|
|
1264
|
+
try:
|
|
1265
|
+
mcmc_obj = MCMCSim(**mcmc)
|
|
1266
|
+
if not isinstance(mcmc_obj.chains, np.ndarray):
|
|
1267
|
+
mcmc_obj.chains = np.array(mcmc_obj.chains)
|
|
1268
|
+
mcmc_obj.model_id = self._normalize_uuid(mcmc_obj.model_id)
|
|
1269
|
+
mcmc_obj.expt_data_id = self._normalize_uuid(mcmc_obj.expt_data_id)
|
|
1270
|
+
self.mcmcs[mcmc_obj.id] = mcmc_obj
|
|
1271
|
+
except Exception as e:
|
|
1272
|
+
logger.warning(f"Skipping invalid serialized MCMC entry: {e}")
|
|
1273
|
+
|
|
1274
|
+
rawtemp = data.get("raw_datas", []) # List of raw data
|
|
1275
|
+
self.raw_datas = {}
|
|
1276
|
+
if rawtemp:
|
|
1277
|
+
for raw in rawtemp:
|
|
1278
|
+
raw_data = RawData(**raw)
|
|
1279
|
+
raw_data.data = pd.DataFrame(
|
|
1280
|
+
raw.get("data", {})
|
|
1281
|
+
)
|
|
1282
|
+
self.raw_datas[raw_data.id] = raw_data # Store by UUID
|
|
1283
|
+
|
|
1284
|
+
expttemp = data.get("expt_datas", []) # List of experimental data
|
|
1285
|
+
self.expt_datas = {}
|
|
1286
|
+
if expttemp:
|
|
1287
|
+
for expt in expttemp:
|
|
1288
|
+
# Ensure backward compatibility - add selected_columns if missing
|
|
1289
|
+
if 'selected_columns' not in expt:
|
|
1290
|
+
expt['selected_columns'] = []
|
|
1291
|
+
|
|
1292
|
+
expt_data = ExptData(**expt)
|
|
1293
|
+
# limiting_shifts now serialized as list of ChemicalShiftParam dicts
|
|
1294
|
+
expt_data.limiting_shifts = {}
|
|
1295
|
+
for cs in expt.get("limiting_shifts", []) or []:
|
|
1296
|
+
csp = ChemicalShiftParam(**cs)
|
|
1297
|
+
key = (csp.species, csp.col)
|
|
1298
|
+
expt_data.limiting_shifts[key] = csp
|
|
1299
|
+
|
|
1300
|
+
# Reconstruct delta_to_spec (object ndarray with floats or lmfit Parameters)
|
|
1301
|
+
serialized_delta = expt.get("delta_to_spec", [])
|
|
1302
|
+
if serialized_delta == []:
|
|
1303
|
+
expt_data.delta_to_spec = None
|
|
1304
|
+
elif isinstance(serialized_delta, list) and serialized_delta:
|
|
1305
|
+
# Expect a list of rows
|
|
1306
|
+
new_rows: list[list[LMFitParameter|float]] = []
|
|
1307
|
+
for row in serialized_delta:
|
|
1308
|
+
reconstructed_row: list[LMFitParameter|float] = []
|
|
1309
|
+
for cell in row:
|
|
1310
|
+
if isinstance(cell, dict) and cell.get("type") == "lmfit_param":
|
|
1311
|
+
# Construct lmfit Parameter
|
|
1312
|
+
parameter = LMFitParameter(
|
|
1313
|
+
name=cell.get("name", ""),
|
|
1314
|
+
value=float(cell.get("value", 0.0)),
|
|
1315
|
+
)
|
|
1316
|
+
if cell.get("min") is not None:
|
|
1317
|
+
parameter.min = float(cell["min"]) # type: ignore[index]
|
|
1318
|
+
if cell.get("max") is not None:
|
|
1319
|
+
parameter.max = float(cell["max"]) # type: ignore[index]
|
|
1320
|
+
parameter.vary = bool(cell.get("vary", True))
|
|
1321
|
+
reconstructed_row.append(parameter)
|
|
1322
|
+
elif cell is None:
|
|
1323
|
+
reconstructed_row.append(0.0)
|
|
1324
|
+
elif isinstance(cell,(str, int, float)):
|
|
1325
|
+
try:
|
|
1326
|
+
reconstructed_row.append(float(cell))
|
|
1327
|
+
except Exception:
|
|
1328
|
+
logger.warning(f"Failed to convert cell {cell} to float, appending 0.0")
|
|
1329
|
+
reconstructed_row.append(0.0)
|
|
1330
|
+
new_rows.append(reconstructed_row)
|
|
1331
|
+
try:
|
|
1332
|
+
nr = len(new_rows)
|
|
1333
|
+
nc = len(new_rows[0]) if nr > 0 else 0
|
|
1334
|
+
# Attempt to create a 2D numpy array with dtype=object
|
|
1335
|
+
# need to do this long-winded approach to stop numpy from
|
|
1336
|
+
# trying (and failing) to coerce the object array to a float array
|
|
1337
|
+
# which calls Parameter.__array__ and fails.
|
|
1338
|
+
if nc > 0:
|
|
1339
|
+
expt_data.delta_to_spec = np.empty((nr, nc), dtype=object)
|
|
1340
|
+
for i in range(nr):
|
|
1341
|
+
for j in range(nc):
|
|
1342
|
+
expt_data.delta_to_spec[i, j] = new_rows[i][j]
|
|
1343
|
+
#expt_data.delta_to_spec = np.array(new_rows, dtype=object)
|
|
1344
|
+
except Exception:
|
|
1345
|
+
# If object array fails, still set as object array coercing to objects
|
|
1346
|
+
expt_data.delta_to_spec = np.array([[x for x in row] for row in new_rows], dtype=object)
|
|
1347
|
+
self.expt_datas[expt_data.id]=expt_data
|
|
1348
|
+
|
|
1349
|
+
self._nComp = int(data.get("nComp", 2))
|
|
1350
|
+
self._nStep = int(data.get("nStep", 20))
|
|
1351
|
+
self._eq_str = data.get("eq_str", "")
|
|
1352
|
+
self._eq_mat_str = data.get("eq_mat_str", "[]")
|
|
1353
|
+
self._eq_mat = np.array(data.get("eq_mat", []))
|
|
1354
|
+
comp_temp = data.get("components", [])
|
|
1355
|
+
self._components = (
|
|
1356
|
+
[Component(**comp) for comp in comp_temp] if comp_temp else []
|
|
1357
|
+
)
|
|
1358
|
+
ktemp = data.get("binding_constants", [])
|
|
1359
|
+
self._binding_constants = [BindingConstant(**k) for k in ktemp] if ktemp else []
|
|
1360
|
+
self._species = data.get("species", [])
|
|
1361
|
+
self._eq_consts = data.get("eq_consts", None)
|
|
1362
|
+
self._model_name = data.get("model_name", "")
|
|
1363
|
+
self._component_names = data.get("component_names", [])
|
|
1364
|
+
self._comp_concs = pd.DataFrame(data.get("comp_concs", {}))
|
|
1365
|
+
self._expt_data = ExptData(**data.get("expt_data", {}))
|
|
1366
|
+
self._active_model_id = self._normalize_uuid(data.get("active_model_id", None))
|
|
1367
|
+
self._active_sim_id = self._normalize_uuid(data.get("active_sim_id", None))
|
|
1368
|
+
self._active_fit_id = self._normalize_uuid(data.get("active_fit_id", None))
|
|
1369
|
+
self._active_raw_data_id = self._normalize_uuid(data.get("active_raw_data_id", None))
|
|
1370
|
+
self._active_expt_data_id = self._normalize_uuid(data.get("active_expt_data_id", None))
|
|
1371
|
+
self._active_mcmc_id = self._normalize_uuid(data.get("active_mcmc_id", None))
|
|
1372
|
+
|
|
1373
|
+
mids = data.get("default_model_ids", None)
|
|
1374
|
+
if mids:
|
|
1375
|
+
self.default_model_ids = [uuid.UUID(m) for m in mids]
|
|
1376
|
+
else:
|
|
1377
|
+
# Backward compatibility for older saved states without default_model_ids.
|
|
1378
|
+
# Any model ID that matches the bundled defaults is treated as built-in.
|
|
1379
|
+
bundled_default_ids = self._default_model_ids_from_bundle()
|
|
1380
|
+
self.default_model_ids = [mid for mid in self.models.keys() if mid in bundled_default_ids]
|
|
1381
|
+
|
|
1382
|
+
#self._active_raw_data_id = data.get("active_raw_data_id", None)
|
|
1383
|
+
for name, dtype in data.get("_expt_dtypes", {}).items():
|
|
1384
|
+
self._expt_dtypes[name] = ExptDataType(**dtype)
|
|
1385
|
+
|
|
1386
|
+
self._finalize_active_context(reason="from_json", emit_events=False)
|
|
1387
|
+
|
|
1388
|
+
def resolve_str_None(self, id: uuid.UUID | str | None):
|
|
1389
|
+
if id == "None":
|
|
1390
|
+
id = None
|
|
1391
|
+
return id
|
|
1392
|
+
|
|
1393
|
+
# def parse_equations(self, e=None):
|
|
1394
|
+
# """Parse the equilibrium equations and update the model data output."""
|
|
1395
|
+
# # This method is now async to allow for UI interactions
|
|
1396
|
+
# return self._parse_equations(e)
|
|
1397
|
+
|
|
1398
|
+
async def parse_equations(self, e):
|
|
1399
|
+
"""Parse the equilibrium equations and update the model data output."""
|
|
1400
|
+
|
|
1401
|
+
# if current model_name is already a model, ask the user if they want to overwrite
|
|
1402
|
+
if self.model_name in [m.name for m in self.models.values()]:
|
|
1403
|
+
#model = [m for m in self.models if m.name == self.model_name][0]
|
|
1404
|
+
model = self.active_model
|
|
1405
|
+
if model.eq_mat_str == "" or model.eq_mat is np.array([]):
|
|
1406
|
+
# then this is a new model and we don't need to ask about overwriting
|
|
1407
|
+
pass
|
|
1408
|
+
# otherwise, we need to ask the user if they want to overwrite the model
|
|
1409
|
+
else:
|
|
1410
|
+
with ui.dialog() as dialog, ui.card():
|
|
1411
|
+
ui.label(
|
|
1412
|
+
f"Are you sure you want to overwrite model \"{self.model_name}\"? \nDoing so will delete all simulations and fits which rely on this model."
|
|
1413
|
+
)
|
|
1414
|
+
with ui.row():
|
|
1415
|
+
ui.button("Yes", on_click=lambda: dialog.submit(True))
|
|
1416
|
+
ui.button("No", on_click=lambda: dialog.submit(False))
|
|
1417
|
+
|
|
1418
|
+
async def show():
|
|
1419
|
+
result = await dialog
|
|
1420
|
+
return result
|
|
1421
|
+
|
|
1422
|
+
res = await show()
|
|
1423
|
+
if res is False: # i.e. user does not want to save
|
|
1424
|
+
ui.notify(
|
|
1425
|
+
"Please give a unique model name and re-try.", type="info"
|
|
1426
|
+
)
|
|
1427
|
+
return
|
|
1428
|
+
else:
|
|
1429
|
+
ui.notify(f"Overwriting model {self.model_name}", type="warning")
|
|
1430
|
+
|
|
1431
|
+
eq_input = self.active_model.eq_str.strip() if self.active_model.eq_str else ""
|
|
1432
|
+
|
|
1433
|
+
if not eq_input:
|
|
1434
|
+
ui.notify("Please enter equilibrium equations.", type="negative")
|
|
1435
|
+
return
|
|
1436
|
+
try:
|
|
1437
|
+
eq_matrix, component_names, species = eq_mat_from_equation_str_infer_components(
|
|
1438
|
+
eq_input
|
|
1439
|
+
)
|
|
1440
|
+
model = self.active_model
|
|
1441
|
+
nmodel = len(self.models)
|
|
1442
|
+
self.delete_model(self.active_model, notify_user=False, notify_listeners=True)
|
|
1443
|
+
if nmodel==1:
|
|
1444
|
+
# if we had only one model, then deletion would have initialized a default model
|
|
1445
|
+
# since we cannot have zero models.
|
|
1446
|
+
self.active_model.name = model.name # Set the name of the default model
|
|
1447
|
+
else:
|
|
1448
|
+
self.new_model(name=model.name) # Create a new model with the same name
|
|
1449
|
+
self.active_model.eq_str = model.eq_str # Copy the equilibrium string from the old model
|
|
1450
|
+
self.active_model.nComp = model.nComp # Copy the number of components from the old model
|
|
1451
|
+
self.active_model.nStep = model.nStep # Copy the number of steps from the
|
|
1452
|
+
self.active_model.component_concs = model.component_concs # Copy the component concentrations from the old model
|
|
1453
|
+
self.active_model.component_names = model.component_names # Copy the component names from the old model
|
|
1454
|
+
self.active_model.components = model.components # Copy the components from the old model
|
|
1455
|
+
#self.notify_listeners('model_changed') # Notify listeners that the model has been updated
|
|
1456
|
+
#self.active_model_idx = self.models.index(model)
|
|
1457
|
+
|
|
1458
|
+
|
|
1459
|
+
|
|
1460
|
+
self.active_model.eq_mat = eq_matrix
|
|
1461
|
+
# TODO replace next line with a property
|
|
1462
|
+
self.active_model.eq_mat_str = str(eq_matrix.tolist()).replace(
|
|
1463
|
+
"],", "]\n"
|
|
1464
|
+
) # Convert to list for display
|
|
1465
|
+
self.species = species
|
|
1466
|
+
self.component_names = component_names
|
|
1467
|
+
|
|
1468
|
+
self.active_model.nComp = len(component_names) # Update number of components in SimData
|
|
1469
|
+
# self.modelData.set_visibility(True) # Show the model data output area
|
|
1470
|
+
|
|
1471
|
+
for comp in component_names:
|
|
1472
|
+
if comp not in [comp.name for comp in self.components]:
|
|
1473
|
+
# Add new component to SimData
|
|
1474
|
+
self.components.append(Component(name=comp))
|
|
1475
|
+
|
|
1476
|
+
self.components = [
|
|
1477
|
+
comp for comp in self.components if comp.name in component_names
|
|
1478
|
+
] # Keep only components that are in the list
|
|
1479
|
+
|
|
1480
|
+
# reorder sd.components to match components list, based on the name attribute of sd.components elements
|
|
1481
|
+
self.components = sorted(
|
|
1482
|
+
self.components,
|
|
1483
|
+
key=lambda x: (
|
|
1484
|
+
component_names.index(x.name) if x.name in component_names else float("inf")
|
|
1485
|
+
),
|
|
1486
|
+
)
|
|
1487
|
+
|
|
1488
|
+
self.generate_binding_constants() # Generate binding constants based on the current model
|
|
1489
|
+
|
|
1490
|
+
|
|
1491
|
+
self.notify_listeners(
|
|
1492
|
+
"model_changed", self.active_model
|
|
1493
|
+
) # Notify listeners that the model has been updated
|
|
1494
|
+
|
|
1495
|
+
# Disable save button when equations are parsed
|
|
1496
|
+
# TODO restore #self.save_sim_details_button.set_enabled(False)
|
|
1497
|
+
# default to model name + values of Ks
|
|
1498
|
+
|
|
1499
|
+
except Exception as e:
|
|
1500
|
+
logger.error(e)
|
|
1501
|
+
ui.notify("Error: " + str(e), type="negative")
|
|
1502
|
+
|
|
1503
|
+
|
|
1504
|
+
def generate_binding_constants(self):
|
|
1505
|
+
"""Generate binding constants based on the current model."""
|
|
1506
|
+
if not self.active_model:
|
|
1507
|
+
ui.notify("No active model to generate binding constants.", type="negative")
|
|
1508
|
+
return
|
|
1509
|
+
|
|
1510
|
+
species = self.active_model.species
|
|
1511
|
+
components = self.active_model.component_names
|
|
1512
|
+
|
|
1513
|
+
# number of bound species:
|
|
1514
|
+
boundspecies = [s for s in species if s not in components]
|
|
1515
|
+
# num_bound_species = len(species) - len(components)
|
|
1516
|
+
|
|
1517
|
+
|
|
1518
|
+
for s in species:
|
|
1519
|
+
|
|
1520
|
+
if s not in [
|
|
1521
|
+
k.species
|
|
1522
|
+
for k in self.active_model.binding_constants
|
|
1523
|
+
if k.species not in species
|
|
1524
|
+
]:
|
|
1525
|
+
logK = None
|
|
1526
|
+
isComp = False
|
|
1527
|
+
vary = True
|
|
1528
|
+
if s not in boundspecies:
|
|
1529
|
+
# this is a component so logK = 0
|
|
1530
|
+
logK = 0
|
|
1531
|
+
isComp = True
|
|
1532
|
+
vary = False
|
|
1533
|
+
self.active_model.binding_constants.append(
|
|
1534
|
+
BindingConstant(species=s, logK=logK, vary=vary, isComp=isComp)
|
|
1535
|
+
)
|
|
1536
|
+
|
|
1537
|
+
# remove stale binding constants
|
|
1538
|
+
# self.sd.binding_constants = [k for k in self.sd.binding_constants if k.species in species]
|
|
1539
|
+
|
|
1540
|
+
# ensure isComp is set correctly for existing binding constants, and remove stale ones
|
|
1541
|
+
for k in self.active_model.binding_constants:
|
|
1542
|
+
if k.species not in species:
|
|
1543
|
+
self.active_model.binding_constants.remove(k)
|
|
1544
|
+
else:
|
|
1545
|
+
if k.species in components:
|
|
1546
|
+
k.isComp = True
|
|
1547
|
+
else:
|
|
1548
|
+
k.isComp = False
|
|
1549
|
+
|
|
1550
|
+
# reorder self.sd.binding_constants to match species list
|
|
1551
|
+
ro = []
|
|
1552
|
+
for s in species:
|
|
1553
|
+
# as a quick check, ensure components have logK=0
|
|
1554
|
+
for k in self.active_model.binding_constants:
|
|
1555
|
+
if k.species in components:
|
|
1556
|
+
k.logK = 0
|
|
1557
|
+
# reorder self.sd.binding_constants to match species list
|
|
1558
|
+
ro.append([k for k in self.active_model.binding_constants if k.species == s][0])
|
|
1559
|
+
|
|
1560
|
+
self.active_model.binding_constants = ro
|
|
1561
|
+
|
|
1562
|
+
def remove_model_dependent_objs(self, model: Model, notify_user: bool = True, notify_listeners: bool = True):
|
|
1563
|
+
"""Backward-compatible helper for model cascade deletion."""
|
|
1564
|
+
if model.id not in self.models:
|
|
1565
|
+
ui.notify("Model not found.", type="negative")
|
|
1566
|
+
return
|
|
1567
|
+
self.delete_model(model, notify_user=notify_user, notify_listeners=notify_listeners)
|
|
1568
|
+
|
|
1569
|
+
def delete_model(self, model: Model, notify_user: bool = True, notify_listeners: bool = True, reconcile: bool = True):
|
|
1570
|
+
"""Delete a model from the state manager."""
|
|
1571
|
+
if model.id in self.default_model_ids:
|
|
1572
|
+
if notify_user:
|
|
1573
|
+
ui.notify("Built-in models cannot be deleted.", type="warning")
|
|
1574
|
+
return
|
|
1575
|
+
|
|
1576
|
+
if model.id in self.models:
|
|
1577
|
+
before = self._snapshot_object_ids()
|
|
1578
|
+
self._delete_object_core(model)
|
|
1579
|
+
if len(self.models) == 0:
|
|
1580
|
+
self._initialize_default_models(emit_events=False)
|
|
1581
|
+
if notify_user:
|
|
1582
|
+
ui.notify("No models left. Default models have been created.", type="info")
|
|
1583
|
+
after = self._snapshot_object_ids()
|
|
1584
|
+
self._emit_collection_events(before, after, notify_listeners=notify_listeners)
|
|
1585
|
+
if reconcile:
|
|
1586
|
+
self._finalize_active_context(reason="delete_model", emit_events=notify_listeners)
|
|
1587
|
+
else:
|
|
1588
|
+
ui.notify("Model not found.", type="negative")
|
|
1589
|
+
|
|
1590
|
+
def delete_fit(self, fit: FitResult, notify_user: bool = True, notify_listeners: bool = True, reconcile: bool = True):
|
|
1591
|
+
"""Delete a fit from the state manager."""
|
|
1592
|
+
if fit.id in self.fits:
|
|
1593
|
+
before = self._snapshot_object_ids()
|
|
1594
|
+
self._delete_object_core(fit)
|
|
1595
|
+
after = self._snapshot_object_ids()
|
|
1596
|
+
self._emit_collection_events(before, after, notify_listeners=notify_listeners)
|
|
1597
|
+
if reconcile:
|
|
1598
|
+
self._finalize_active_context(reason="delete_fit", emit_events=notify_listeners)
|
|
1599
|
+
else:
|
|
1600
|
+
ui.notify("Fit not found.", type="negative")
|
|
1601
|
+
|
|
1602
|
+
def delete_simulation(self, simulation: Simulation, notify_user: bool = True, notify_listeners: bool = True, reconcile: bool = True):
|
|
1603
|
+
"""Delete a simulation from the state manager."""
|
|
1604
|
+
if simulation.id in self.simulations:
|
|
1605
|
+
before = self._snapshot_object_ids()
|
|
1606
|
+
self._delete_object_core(simulation)
|
|
1607
|
+
after = self._snapshot_object_ids()
|
|
1608
|
+
self._emit_collection_events(before, after, notify_listeners=notify_listeners)
|
|
1609
|
+
if reconcile:
|
|
1610
|
+
self._finalize_active_context(reason="delete_simulation", emit_events=notify_listeners)
|
|
1611
|
+
else:
|
|
1612
|
+
ui.notify("Simulation not found.", type="negative")
|
|
1613
|
+
|
|
1614
|
+
def delete_expt_data(self, expt_data: ExptData, notify_user: bool = True, notify_listeners: bool = True, reconcile: bool = True):
|
|
1615
|
+
"""Delete experimental data from the state manager."""
|
|
1616
|
+
if expt_data.id in self.expt_datas:
|
|
1617
|
+
before = self._snapshot_object_ids()
|
|
1618
|
+
self._delete_object_core(expt_data)
|
|
1619
|
+
after = self._snapshot_object_ids()
|
|
1620
|
+
self._emit_collection_events(before, after, notify_listeners=notify_listeners)
|
|
1621
|
+
if reconcile:
|
|
1622
|
+
self._finalize_active_context(reason="delete_expt_data", emit_events=notify_listeners)
|
|
1623
|
+
else:
|
|
1624
|
+
ui.notify("Experimental data not found.", type="negative")
|
|
1625
|
+
|
|
1626
|
+
def delete_raw_data(self, raw_data: RawData, notify_user: bool = True, notify_listeners: bool = True, reconcile: bool = True):
|
|
1627
|
+
"""Delete raw data from the state manager."""
|
|
1628
|
+
if raw_data.id in self.raw_datas:
|
|
1629
|
+
before = self._snapshot_object_ids()
|
|
1630
|
+
self._delete_object_core(raw_data)
|
|
1631
|
+
after = self._snapshot_object_ids()
|
|
1632
|
+
self._emit_collection_events(before, after, notify_listeners=notify_listeners)
|
|
1633
|
+
if reconcile:
|
|
1634
|
+
self._finalize_active_context(reason="delete_raw_data", emit_events=notify_listeners)
|
|
1635
|
+
else:
|
|
1636
|
+
ui.notify("Raw data not found.", type="negative")
|
|
1637
|
+
|
|
1638
|
+
def delete_mcmc(self, mcmc: MCMCSim, notify_user: bool = True, notify_listeners: bool = True, reconcile: bool = True):
|
|
1639
|
+
"""Delete MCMC results from the state manager."""
|
|
1640
|
+
if mcmc.id in self.mcmcs:
|
|
1641
|
+
before = self._snapshot_object_ids()
|
|
1642
|
+
self._delete_object_core(mcmc)
|
|
1643
|
+
after = self._snapshot_object_ids()
|
|
1644
|
+
self._emit_collection_events(before, after, notify_listeners=notify_listeners)
|
|
1645
|
+
if reconcile:
|
|
1646
|
+
self._finalize_active_context(reason="delete_mcmc", emit_events=notify_listeners)
|
|
1647
|
+
else:
|
|
1648
|
+
ui.notify("MCMC result not found.", type="negative")
|
|
1649
|
+
|
|
1650
|
+
def update_ui_bindings(self, bindables: str|list|None=None):
|
|
1651
|
+
if bindables is None:
|
|
1652
|
+
bindables = list(self.ui_bindings.__dict__.keys())
|
|
1653
|
+
|
|
1654
|
+
def set_bindable(dest,src,parent=None):
|
|
1655
|
+
if parent is not None:
|
|
1656
|
+
if not hasattr(self,parent) or getattr(self,parent) is None:
|
|
1657
|
+
val = ""
|
|
1658
|
+
else:
|
|
1659
|
+
val = getattr(getattr(self,parent),src)
|
|
1660
|
+
else:
|
|
1661
|
+
val = getattr(self,src)
|
|
1662
|
+
if hasattr(self.ui_bindings, dest):
|
|
1663
|
+
setattr(self.ui_bindings, dest, val)
|
|
1664
|
+
|
|
1665
|
+
|
|
1666
|
+
for b in bindables:
|
|
1667
|
+
if hasattr(self.ui_bindings, b):
|
|
1668
|
+
if b == 'model_name':
|
|
1669
|
+
set_bindable(b, 'name', parent='active_model')
|
|
1670
|
+
|
|
1671
|
+
|
|
1672
|
+
def dump_setup_and_model_def(self,model:Model) ->str:
|
|
1673
|
+
outstr = f"""
|
|
1674
|
+
import bindtools as bd
|
|
1675
|
+
import numpy as np
|
|
1676
|
+
import matplotlib.pyplot as plt
|
|
1677
|
+
|
|
1678
|
+
#model = Model(name={model.name}, eq_mat={repr(model.eq_mat.tolist())}))
|
|
1679
|
+
|
|
1680
|
+
eq_mat = {model.eq_mat.tolist()}
|
|
1681
|
+
model_name = {model.name}
|
|
1682
|
+
|
|
1683
|
+
comp_names = {model.component_names}
|
|
1684
|
+
species_names = {model.species}
|
|
1685
|
+
nstep = {model.nStep}
|
|
1686
|
+
|
|
1687
|
+
"""
|
|
1688
|
+
return outstr
|
|
1689
|
+
|
|
1690
|
+
def dump_simulation_to_python(self, model: Model) -> str:
|
|
1691
|
+
# dump initial imports and model setup
|
|
1692
|
+
outstr = self.dump_setup_and_model_def(model)
|
|
1693
|
+
# generate component blocks
|
|
1694
|
+
for c in model.components:
|
|
1695
|
+
if c.spacing == 'lin':
|
|
1696
|
+
outstr += f"\n{c.name}_concs = np.linspace({c.start_conc if c.start_conc is not None else 0},{c.end_conc if c.end_conc is not None else 0},nstep)"
|
|
1697
|
+
else:
|
|
1698
|
+
raise(NotImplementedError)
|
|
1699
|
+
|
|
1700
|
+
outstr += f"""
|
|
1701
|
+
comp_concs = np.vstack(({','.join([c+'_concs' for c in model.component_names])}))
|
|
1702
|
+
|
|
1703
|
+
logKs = dict()
|
|
1704
|
+
for i,s in enumerate(species_names):
|
|
1705
|
+
logKs[s] = model.binding_constants[i].logK
|
|
1706
|
+
|
|
1707
|
+
|
|
1708
|
+
|
|
1709
|
+
# model parameters are all defined
|
|
1710
|
+
# now let's generate the model and simulate it
|
|
1711
|
+
|
|
1712
|
+
model = bd.bindingModel(eq_mat, comp_names, species_names,compConcs=comp_concs)
|
|
1713
|
+
model.prepModel()
|
|
1714
|
+
model.params.pretty_print()
|
|
1715
|
+
|
|
1716
|
+
for s in species_names:
|
|
1717
|
+
model.params[s].set(value=logKs[s])
|
|
1718
|
+
|
|
1719
|
+
spec=model.calcSpeciation()
|
|
1720
|
+
|
|
1721
|
+
"""
|
|
1722
|
+
|
|
1723
|
+
|
|
1724
|
+
return outstr
|
|
1725
|
+
|
|
1726
|
+
def dump_fit_to_python(self, fit: FitResult) -> str:
|
|
1727
|
+
outstr = self.dump_setup_and_model_def(fit.model)
|
|
1728
|
+
|
|
1729
|
+
bm = self.generate_binding_model_for_fit(fit)
|
|
1730
|
+
|
|
1731
|
+
# add some imports
|
|
1732
|
+
outstr = """
|
|
1733
|
+
import pandas as pd
|
|
1734
|
+
import lmfit
|
|
1735
|
+
""" + outstr
|
|
1736
|
+
|
|
1737
|
+
# load datafile
|
|
1738
|
+
outstr += f"""
|
|
1739
|
+
#load datafile
|
|
1740
|
+
data = pd.read_csv({fit.expt_data.raw_data.filename})
|
|
1741
|
+
|
|
1742
|
+
# sort datafile
|
|
1743
|
+
column_mapping = {repr(fit.expt_data.column_mapping)}
|
|
1744
|
+
if column_mapping is not None:
|
|
1745
|
+
old_cols = self.data.columns
|
|
1746
|
+
new_cols = [None] * len(old_cols)
|
|
1747
|
+
for raw_proc in self.column_mapping:
|
|
1748
|
+
new_cols[proc] = old_cols[raw]
|
|
1749
|
+
data = data[new_cols]"""
|
|
1750
|
+
|
|
1751
|
+
# specify compconc etc matrices
|
|
1752
|
+
outstr += f"""
|
|
1753
|
+
# various matrices
|
|
1754
|
+
obs_list =
|
|
1755
|
+
component_names = {repr(bm.compNames)}
|
|
1756
|
+
integ_to_spec = {repr(bm.colToSpec)}
|
|
1757
|
+
delta_to_spec = {repr(bm.specToDd)}
|
|
1758
|
+
species = {repr(bm.plist)}
|
|
1759
|
+
col_to_comp = {repr(bm.colToComp)}"""
|
|
1760
|
+
|
|
1761
|
+
# set up fit
|
|
1762
|
+
outstr += """
|
|
1763
|
+
# fitting and plotting routines
|
|
1764
|
+
fit = bd.bindingModel(eqMat=eqMat,
|
|
1765
|
+
compNames=component_names,
|
|
1766
|
+
speciesList=species,
|
|
1767
|
+
colToComp=col_to_comp,
|
|
1768
|
+
specToInteg=integ_to_spec,
|
|
1769
|
+
specToDd=delta_to_spec,
|
|
1770
|
+
obsList=obs_list,
|
|
1771
|
+
rawData=data)
|
|
1772
|
+
#sigma = [0.0005, 0.0005,0.0005,0.0005,0.0005]
|
|
1773
|
+
fit.prepModel()
|
|
1774
|
+
"""
|
|
1775
|
+
outstr += "# Define parameters"
|
|
1776
|
+
for k in fit.model.binding_constants:
|
|
1777
|
+
outstr += f"\nfit.params['log{k.species}'].set(value={k.logK if k.logK is not None else 0.0}, vary={k.vary}, min={k.min}, max={k.max})"
|
|
1778
|
+
|
|
1779
|
+
|
|
1780
|
+
outstr += f"""
|
|
1781
|
+
fit.runModel(sigma=sigma,skip_col=2,method={fit.fit_method})
|
|
1782
|
+
bd.makeFitResidPlot(fit,plotMask=(0,1),ylabel='Chemical shift (ppm)')"""
|
|
1783
|
+
|
|
1784
|
+
|
|
1785
|
+
|
|
1786
|
+
return outstr
|
|
1787
|
+
# model_str = self.dump_model_to_python(fit._model) if fit._model is not None else "# Model not linked."
|
|
1788
|
+
# return model_str
|
|
1789
|
+
|
|
1790
|
+
def generate_binding_model_for_fit(self, fit: Optional[FitResult]=None, analytical_cfg: Optional[dict[str, object]]=None) -> bd.bindingModel:
|
|
1791
|
+
"""Generate a bindtools.bindingModel from the current active model."""
|
|
1792
|
+
old_fit = None
|
|
1793
|
+
if fit is not None and fit.id != self.active_fit_id:
|
|
1794
|
+
old_fit = str(self.active_fit_id)
|
|
1795
|
+
self.active_fit_id = fit.id
|
|
1796
|
+
self.active_expt_data_id = fit.expt_data_id
|
|
1797
|
+
self.active_model_id = fit.model_id
|
|
1798
|
+
|
|
1799
|
+
if not self.active_model:
|
|
1800
|
+
raise ValueError("No active model to generate binding model.")
|
|
1801
|
+
|
|
1802
|
+
obs_list = [x for x in self.active_expt_data.sorted_data.columns if self.active_expt_data.col_details[x]['depindep'] == 'dep']
|
|
1803
|
+
integ_to_spec = self.active_expt_data.integ_to_spec
|
|
1804
|
+
if not (isinstance(integ_to_spec, np.ndarray) and integ_to_spec.ndim == 2 and integ_to_spec.size > 0):
|
|
1805
|
+
integ_to_spec = None
|
|
1806
|
+
delta_to_spec = self.active_expt_data.delta_to_spec
|
|
1807
|
+
if not (isinstance(delta_to_spec, np.ndarray) and delta_to_spec.ndim == 2 and delta_to_spec.size > 0):
|
|
1808
|
+
delta_to_spec = None
|
|
1809
|
+
|
|
1810
|
+
model = bd.bindingModel(
|
|
1811
|
+
eqMat=self.active_model.eq_mat,
|
|
1812
|
+
compNames=self.active_model.component_names,
|
|
1813
|
+
speciesList=self.active_model.species,
|
|
1814
|
+
colToComp=self.active_expt_data.col_to_comp,
|
|
1815
|
+
specToInteg=integ_to_spec,
|
|
1816
|
+
specToDd = delta_to_spec.T if delta_to_spec is not None else None,
|
|
1817
|
+
obsList=obs_list,
|
|
1818
|
+
rawData=np.array(self.active_expt_data.sorted_data)
|
|
1819
|
+
)
|
|
1820
|
+
|
|
1821
|
+
cfg = analytical_cfg
|
|
1822
|
+
if cfg is None and fit is not None and getattr(fit, "analytical_fast_exchange", False):
|
|
1823
|
+
cfg = {
|
|
1824
|
+
"topology": fit.analytical_topology,
|
|
1825
|
+
"complex_indices": list(getattr(fit, "analytical_complex_indices", [])),
|
|
1826
|
+
"obs_columns": list(getattr(fit, "analytical_obs_columns", [])),
|
|
1827
|
+
"obs_components": list(getattr(fit, "analytical_obs_components", [])),
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
if cfg is not None:
|
|
1831
|
+
model.analytical_fast_exchange = True
|
|
1832
|
+
model.analytical_topology = str(cfg["topology"])
|
|
1833
|
+
model.analytical_complex_indices = [int(x) for x in cfg["complex_indices"]] # type: ignore[index]
|
|
1834
|
+
model.analytical_obs_columns = [str(x) for x in cfg["obs_columns"]] # type: ignore[index]
|
|
1835
|
+
model.analytical_obs_components = [int(x) for x in cfg["obs_components"]] # type: ignore[index]
|
|
1836
|
+
logger.info(
|
|
1837
|
+
"Using analytical fast-exchange backend (%s model) for fitting.",
|
|
1838
|
+
model.analytical_topology,
|
|
1839
|
+
)
|
|
1840
|
+
|
|
1841
|
+
# Handle UV-vis / fluorescence linear observables (numerical and analytical paths).
|
|
1842
|
+
if self.active_expt_data.has_linear_obs(self._expt_dtypes):
|
|
1843
|
+
self.active_expt_data.build_abs_to_spec(self._expt_dtypes)
|
|
1844
|
+
abs_to_spec = self.active_expt_data.abs_to_spec
|
|
1845
|
+
if abs_to_spec is not None and isinstance(abs_to_spec, np.ndarray) and abs_to_spec.ndim == 2 and abs_to_spec.size > 0:
|
|
1846
|
+
model.specToLinear = abs_to_spec.T # (n_species, n_obs)
|
|
1847
|
+
|
|
1848
|
+
# Build per-observable param name map for the analytical path.
|
|
1849
|
+
linear_obs_param_map: list[list] = []
|
|
1850
|
+
for obs_idx in range(abs_to_spec.shape[0]):
|
|
1851
|
+
pnames: list = []
|
|
1852
|
+
for species_idx in range(abs_to_spec.shape[1]):
|
|
1853
|
+
cell = abs_to_spec[obs_idx, species_idx]
|
|
1854
|
+
pnames.append(cell.name if isinstance(cell, LMFitParameter) else None)
|
|
1855
|
+
linear_obs_param_map.append(pnames)
|
|
1856
|
+
|
|
1857
|
+
lin_cols = [col for col in obs_list
|
|
1858
|
+
if self.active_expt_data.col_details.get(col, {}).get("depindep") == "dep"
|
|
1859
|
+
# already filtered above — just iterate obs_list directly
|
|
1860
|
+
]
|
|
1861
|
+
model.analytical_linear_obs_columns = lin_cols
|
|
1862
|
+
model.analytical_linear_obs_param_map = linear_obs_param_map
|
|
1863
|
+
|
|
1864
|
+
model.prepModel()
|
|
1865
|
+
|
|
1866
|
+
for k in self.active_model.binding_constants:
|
|
1867
|
+
model.params[f'log{k.species}'].set(value=k.logK if k.logK is not None else 0.0, vary=k.vary, min=k.min, max=k.max)
|
|
1868
|
+
|
|
1869
|
+
|
|
1870
|
+
# cleanup by restoring previous fit if needed
|
|
1871
|
+
if old_fit is not None:
|
|
1872
|
+
self.active_fit_id = uuid.UUID(old_fit)
|
|
1873
|
+
self.active_expt_data_id = self.active_fit.expt_data_id
|
|
1874
|
+
self.active_model_id = self.active_fit.model_id
|
|
1875
|
+
|
|
1876
|
+
return model
|
|
1877
|
+
|
|
1878
|
+
# ------------------------------------------------------------------
|
|
1879
|
+
# Jupyter notebook export
|
|
1880
|
+
# ------------------------------------------------------------------
|
|
1881
|
+
|
|
1882
|
+
def dump_simulation_notebook(self, sim: Simulation) -> dict:
|
|
1883
|
+
"""Export *sim* as an nbformat-4 notebook dict.
|
|
1884
|
+
|
|
1885
|
+
The notebook embeds component concentrations from ``sim.comp_concs``
|
|
1886
|
+
and uses the binding constants from the linked model. It is entirely
|
|
1887
|
+
self-contained (no nicegui / webgui imports).
|
|
1888
|
+
"""
|
|
1889
|
+
from webgui.export.notebook_exporter import export_simulation_notebook
|
|
1890
|
+
|
|
1891
|
+
model = self.models.get(sim.model_id)
|
|
1892
|
+
if model is None:
|
|
1893
|
+
raise ValueError(f"Model {sim.model_id} not found for simulation.")
|
|
1894
|
+
return export_simulation_notebook(sim, model)
|
|
1895
|
+
|
|
1896
|
+
def dump_fit_notebook(self, fit: FitResult) -> tuple[dict, "pd.DataFrame"]:
|
|
1897
|
+
"""Export *fit* as an nbformat-4 notebook dict plus a CSV DataFrame.
|
|
1898
|
+
|
|
1899
|
+
The notebook is set up to re-run the fit; original fitted values
|
|
1900
|
+
appear as inline comments. The companion CSV DataFrame contains the
|
|
1901
|
+
raw experimental data and should be saved as ``data.csv`` alongside
|
|
1902
|
+
the exported notebook.
|
|
1903
|
+
|
|
1904
|
+
Returns
|
|
1905
|
+
-------
|
|
1906
|
+
notebook : dict
|
|
1907
|
+
nbformat-4 compatible dict (serialise with ``json.dumps``).
|
|
1908
|
+
csv_df : pd.DataFrame
|
|
1909
|
+
Raw data to be saved as CSV.
|
|
1910
|
+
"""
|
|
1911
|
+
import pandas as pd # noqa: F401 — type annotation only
|
|
1912
|
+
from webgui.export.notebook_exporter import export_fit_notebook
|
|
1913
|
+
|
|
1914
|
+
# Ensure all objects are linked
|
|
1915
|
+
if not hasattr(fit, "_model") or fit._model is None:
|
|
1916
|
+
fit.find_and_link_model(self.models)
|
|
1917
|
+
if not hasattr(fit, "_expt_data") or fit._expt_data is None:
|
|
1918
|
+
fit.find_and_link_expt_data(self.expt_datas)
|
|
1919
|
+
expt_data = fit._expt_data
|
|
1920
|
+
if expt_data is not None:
|
|
1921
|
+
obs_type_names: list[str] = [
|
|
1922
|
+
ot.name for ot in expt_data.get_obs_list(self._expt_dtypes)
|
|
1923
|
+
]
|
|
1924
|
+
if not hasattr(expt_data, "_raw_data") or expt_data._raw_data is None:
|
|
1925
|
+
expt_data.find_and_link_raw_data(self.raw_datas)
|
|
1926
|
+
if not hasattr(expt_data, "_model") or expt_data._model is None:
|
|
1927
|
+
expt_data.find_and_link_model(self.models)
|
|
1928
|
+
else:
|
|
1929
|
+
obs_type_names = []
|
|
1930
|
+
|
|
1931
|
+
# Extract linear observable (UV-vis / fluorescence) structure for the notebook.
|
|
1932
|
+
lin_obs_col_names: list[str] | None = None
|
|
1933
|
+
lin_obs_param_map: list[list] | None = None
|
|
1934
|
+
if expt_data is not None and expt_data.has_linear_obs(self._expt_dtypes):
|
|
1935
|
+
expt_data.build_abs_to_spec(self._expt_dtypes)
|
|
1936
|
+
abs_to_spec = expt_data.abs_to_spec
|
|
1937
|
+
if abs_to_spec is not None and abs_to_spec.ndim == 2 and abs_to_spec.size > 0:
|
|
1938
|
+
lin_obs_col_names = [col for col, _ in expt_data.linear_obs_cols(self._expt_dtypes)]
|
|
1939
|
+
lin_obs_param_map = []
|
|
1940
|
+
for obs_idx in range(abs_to_spec.shape[0]):
|
|
1941
|
+
row: list = []
|
|
1942
|
+
for spec_idx in range(abs_to_spec.shape[1]):
|
|
1943
|
+
cell = abs_to_spec[obs_idx, spec_idx]
|
|
1944
|
+
if isinstance(cell, LMFitParameter):
|
|
1945
|
+
row.append({"name": cell.name, "min": float(cell.min), "max": float(cell.max)})
|
|
1946
|
+
else:
|
|
1947
|
+
row.append(None)
|
|
1948
|
+
lin_obs_param_map.append(row)
|
|
1949
|
+
|
|
1950
|
+
|
|
1951
|
+
|
|
1952
|
+
|
|
1953
|
+
return export_fit_notebook(
|
|
1954
|
+
fit, fit._model, expt_data, expt_data._raw_data,
|
|
1955
|
+
obs_type_names=obs_type_names,
|
|
1956
|
+
lin_obs_col_names=lin_obs_col_names,
|
|
1957
|
+
lin_obs_param_map=lin_obs_param_map,
|
|
1958
|
+
)
|
|
1959
|
+
|
|
1960
|
+
def dump_mcmc_notebook(
|
|
1961
|
+
self,
|
|
1962
|
+
mcmc: "MCMCSim | None",
|
|
1963
|
+
include_chains: bool,
|
|
1964
|
+
) -> "tuple[dict, pd.DataFrame, bytes | None]":
|
|
1965
|
+
"""Export an MCMC run as a notebook zip bundle.
|
|
1966
|
+
|
|
1967
|
+
The notebook contains the full fit setup (identical to
|
|
1968
|
+
:meth:`dump_fit_notebook`) followed by a live MCMC section.
|
|
1969
|
+
|
|
1970
|
+
Parameters
|
|
1971
|
+
----------
|
|
1972
|
+
mcmc : MCMCSim or None
|
|
1973
|
+
The MCMC run to export. When *None* (no run yet), a code-only
|
|
1974
|
+
notebook is generated using the active fit's context.
|
|
1975
|
+
include_chains : bool
|
|
1976
|
+
When True, the companion HDF5 chain file is also produced.
|
|
1977
|
+
Requires ``mcmc.mc.sampler`` to be non-None.
|
|
1978
|
+
|
|
1979
|
+
Returns
|
|
1980
|
+
-------
|
|
1981
|
+
notebook : dict
|
|
1982
|
+
csv_df : pd.DataFrame
|
|
1983
|
+
h5_bytes : bytes or None
|
|
1984
|
+
"""
|
|
1985
|
+
import pandas as pd # noqa: F401
|
|
1986
|
+
from webgui.export.notebook_exporter import export_mcmc_notebook
|
|
1987
|
+
|
|
1988
|
+
# Always use active_fit so that the fit name embedded in the notebook's
|
|
1989
|
+
# read_csv call matches the ZIP entry name produced by the caller.
|
|
1990
|
+
# The MCMCSim is used only for its MCMC parameters and chains.
|
|
1991
|
+
fit = self.active_fit # raises if not set
|
|
1992
|
+
|
|
1993
|
+
# Link model onto fit
|
|
1994
|
+
if not hasattr(fit, "_model") or fit._model is None:
|
|
1995
|
+
fit.find_and_link_model(self.models)
|
|
1996
|
+
|
|
1997
|
+
# Link expt_data onto fit
|
|
1998
|
+
if not hasattr(fit, "_expt_data") or fit._expt_data is None:
|
|
1999
|
+
fit.find_and_link_expt_data(self.expt_datas)
|
|
2000
|
+
expt_data = fit._expt_data
|
|
2001
|
+
|
|
2002
|
+
if expt_data is not None:
|
|
2003
|
+
if not hasattr(expt_data, "_raw_data") or expt_data._raw_data is None:
|
|
2004
|
+
expt_data.find_and_link_raw_data(self.raw_datas)
|
|
2005
|
+
if not hasattr(expt_data, "_model") or expt_data._model is None:
|
|
2006
|
+
expt_data.find_and_link_model(self.models)
|
|
2007
|
+
|
|
2008
|
+
if expt_data is None:
|
|
2009
|
+
raise ValueError("No experimental data found for MCMC notebook export.")
|
|
2010
|
+
if fit._model is None:
|
|
2011
|
+
raise ValueError("No model found for MCMC notebook export.")
|
|
2012
|
+
if expt_data._raw_data is None:
|
|
2013
|
+
raise ValueError("No raw data found for MCMC notebook export.")
|
|
2014
|
+
|
|
2015
|
+
obs_type_names: list[str] = [
|
|
2016
|
+
ot.name for ot in expt_data.get_obs_list(self._expt_dtypes)
|
|
2017
|
+
]
|
|
2018
|
+
|
|
2019
|
+
# Extract linear observable (UV-vis / fluorescence) structure for the notebook.
|
|
2020
|
+
lin_obs_col_names: list[str] | None = None
|
|
2021
|
+
lin_obs_param_map: list[list] | None = None
|
|
2022
|
+
if expt_data.has_linear_obs(self._expt_dtypes):
|
|
2023
|
+
expt_data.build_abs_to_spec(self._expt_dtypes)
|
|
2024
|
+
abs_to_spec = expt_data.abs_to_spec
|
|
2025
|
+
if abs_to_spec is not None and abs_to_spec.ndim == 2 and abs_to_spec.size > 0:
|
|
2026
|
+
lin_obs_col_names = [col for col, _ in expt_data.linear_obs_cols(self._expt_dtypes)]
|
|
2027
|
+
lin_obs_param_map = []
|
|
2028
|
+
for obs_idx in range(abs_to_spec.shape[0]):
|
|
2029
|
+
row: list = []
|
|
2030
|
+
for spec_idx in range(abs_to_spec.shape[1]):
|
|
2031
|
+
cell = abs_to_spec[obs_idx, spec_idx]
|
|
2032
|
+
if isinstance(cell, LMFitParameter):
|
|
2033
|
+
row.append({"name": cell.name, "min": float(cell.min), "max": float(cell.max)})
|
|
2034
|
+
else:
|
|
2035
|
+
row.append(None)
|
|
2036
|
+
lin_obs_param_map.append(row)
|
|
2037
|
+
|
|
2038
|
+
return export_mcmc_notebook(
|
|
2039
|
+
mcmc, fit, fit._model, expt_data, expt_data._raw_data,
|
|
2040
|
+
obs_type_names, include_chains,
|
|
2041
|
+
lin_obs_col_names=lin_obs_col_names,
|
|
2042
|
+
lin_obs_param_map=lin_obs_param_map,
|
|
2043
|
+
)
|