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,173 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
from dataclasses import asdict, dataclass, field, InitVar
|
|
3
|
+
from typing import Optional,Any
|
|
4
|
+
import unicodedata
|
|
5
|
+
from nicegui import binding
|
|
6
|
+
import numpy as np
|
|
7
|
+
import pandas as pd
|
|
8
|
+
from lmfit import Parameter as LMFitParameter
|
|
9
|
+
from .Model import Model
|
|
10
|
+
from .ExptData import ExptData
|
|
11
|
+
import bindtools.binding as bd
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class FitResult:
|
|
15
|
+
"""Data class to represent the results of a fit."""
|
|
16
|
+
model_id: uuid.UUID # The modelUID used for the fit
|
|
17
|
+
expt_data_id: uuid.UUID # The experimental data used for the fit, if any
|
|
18
|
+
name: str # Name of the fit result
|
|
19
|
+
description: str # Description of the fit result
|
|
20
|
+
aic: float # Akaike Information Criterion for the fit
|
|
21
|
+
bic: float # Bayesian Information Criterion for the fit
|
|
22
|
+
chisqr: float # Chi-squared value for the fit
|
|
23
|
+
termination_message: str # Message indicating the termination status of the fit
|
|
24
|
+
fit_method: str = 'least_squares' # method used in the fit e.g. least_sq
|
|
25
|
+
success: Any = False # Whether the fit was successful
|
|
26
|
+
fit_speciation: pd.DataFrame = field(default_factory=pd.DataFrame, compare=False) # DataFrame to hold output species concentrations
|
|
27
|
+
calc_obs: pd.DataFrame = field(default_factory=pd.DataFrame, compare=False) # Calculated observations from the fit
|
|
28
|
+
id: uuid.UUID = field(default_factory=lambda: (uuid.uuid4()))
|
|
29
|
+
params: dict = field(default_factory=dict) # List of binding constants and other parameters
|
|
30
|
+
bd_model: Optional[bd.bindingModel] = None
|
|
31
|
+
analytical_fast_exchange: bool = False
|
|
32
|
+
analytical_topology: Optional[str] = None
|
|
33
|
+
analytical_obs_columns: list[str] = field(default_factory=list)
|
|
34
|
+
analytical_obs_components: list[int] = field(default_factory=list)
|
|
35
|
+
analytical_complex_indices: list[int] = field(default_factory=list)
|
|
36
|
+
|
|
37
|
+
init_model: InitVar[Optional[Model]] = None # The model used for the fit, if any
|
|
38
|
+
init_expt_data: InitVar[Optional[ExptData]] = None # The
|
|
39
|
+
|
|
40
|
+
_model: Optional[Model] = None # The model used for the fit, if any
|
|
41
|
+
_expt_data: Optional[ExptData] = None # The experimental data used for the fit, if any
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def __post_init__(self,init_model,init_expt_data):
|
|
46
|
+
"""Ensure data are appropriate types."""
|
|
47
|
+
if isinstance(init_model,Model):
|
|
48
|
+
self._model = init_model
|
|
49
|
+
self.model_id = init_model.id
|
|
50
|
+
if isinstance(init_expt_data, ExptData):
|
|
51
|
+
self._expt_data = init_expt_data
|
|
52
|
+
self.expt_data_id = init_expt_data.id
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
if not isinstance(self.expt_data_id, uuid.UUID):
|
|
56
|
+
if isinstance(self.expt_data_id, str):
|
|
57
|
+
self.expt_data_id = uuid.UUID(self.expt_data_id)
|
|
58
|
+
|
|
59
|
+
if not isinstance(self.model_id, uuid.UUID):
|
|
60
|
+
if isinstance(self.model_id, str):
|
|
61
|
+
self.model_id = uuid.UUID(self.model_id)
|
|
62
|
+
|
|
63
|
+
if not isinstance(self.id, uuid.UUID):
|
|
64
|
+
if isinstance(self.id, str):
|
|
65
|
+
self.id = uuid.UUID(self.id)
|
|
66
|
+
|
|
67
|
+
if isinstance(self._expt_data,ExptData) and not self.expt_data_id:
|
|
68
|
+
self.expt_data_id = self._expt_data.id
|
|
69
|
+
|
|
70
|
+
if isinstance(self._model, Model) and not self.model_id:
|
|
71
|
+
self.model_id = self._model.id
|
|
72
|
+
|
|
73
|
+
def __eq__(self,other):
|
|
74
|
+
if not isinstance(other, FitResult):
|
|
75
|
+
return False
|
|
76
|
+
|
|
77
|
+
return self.id == other.id
|
|
78
|
+
|
|
79
|
+
def find_and_link_expt_data(self, expt_datas: dict[uuid.UUID,ExptData]) -> None:
|
|
80
|
+
"""Link the experimental data to this fit result."""
|
|
81
|
+
if expt_datas is not None:
|
|
82
|
+
if self.expt_data_id in expt_datas and self.expt_data_id is not None:
|
|
83
|
+
self._expt_data = expt_datas[self.expt_data_id]
|
|
84
|
+
return
|
|
85
|
+
else:
|
|
86
|
+
raise ValueError(f"Corresponding experimental data {self.expt_data_id} not found for FitResult.")
|
|
87
|
+
|
|
88
|
+
def find_and_link_model(self, models: dict[uuid.UUID,Model]) -> None:
|
|
89
|
+
"""Link the experimental data to this fit result."""
|
|
90
|
+
if models is not None:
|
|
91
|
+
if self.model_id in models and self.model_id is not None:
|
|
92
|
+
self._model = models[self.model_id]
|
|
93
|
+
return
|
|
94
|
+
else:
|
|
95
|
+
raise ValueError(f"Corresponding model {self.model_id} not found for FitResult.")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def comp_concs(self) -> pd.DataFrame:
|
|
100
|
+
"""Get the component concentrations for this fit."""
|
|
101
|
+
if self._expt_data:
|
|
102
|
+
return self._expt_data.comp_concs
|
|
103
|
+
else:
|
|
104
|
+
raise ValueError("ExptData is not set or does not have component concentrations.")
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def model(self) -> Model:
|
|
108
|
+
"""Get the model associated with this fit."""
|
|
109
|
+
if hasattr(self, "_model") and isinstance(self._model, Model):
|
|
110
|
+
return self._model
|
|
111
|
+
else:
|
|
112
|
+
raise ValueError("Model is not set for FitResult.")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@model.setter
|
|
116
|
+
def model(self, model: Model) -> None:
|
|
117
|
+
"""Set the model for this fit."""
|
|
118
|
+
if model is not None:
|
|
119
|
+
self.model_id = model.id
|
|
120
|
+
self._model = model
|
|
121
|
+
else:
|
|
122
|
+
raise ValueError("Model cannot be None for FitResult.")
|
|
123
|
+
|
|
124
|
+
@property
|
|
125
|
+
def expt_data(self) -> ExptData:
|
|
126
|
+
"""Get the experimental data associated with this fit."""
|
|
127
|
+
if hasattr(self, "_expt_data") and isinstance(self._expt_data, ExptData):
|
|
128
|
+
return self._expt_data
|
|
129
|
+
else:
|
|
130
|
+
raise ValueError("ExptData is not set for FitResult.")
|
|
131
|
+
|
|
132
|
+
@expt_data.setter
|
|
133
|
+
def expt_data(self, expt_data: ExptData) -> None:
|
|
134
|
+
"""Set the experimental data for this fit."""
|
|
135
|
+
if expt_data is not None:
|
|
136
|
+
self.expt_data_id = expt_data.id
|
|
137
|
+
self._expt_data = expt_data
|
|
138
|
+
else:
|
|
139
|
+
raise ValueError("ExptData cannot be None for FitResult.")
|
|
140
|
+
|
|
141
|
+
def to_dict(self) -> dict[str, str|dict|list|bool|float|None]:
|
|
142
|
+
"""Convert FitResult to a dictionary."""
|
|
143
|
+
return {
|
|
144
|
+
"model_id": str(self.model_id) if self.model_id else "",
|
|
145
|
+
"expt_data_id": str(self.expt_data_id) if self.expt_data_id else None,
|
|
146
|
+
"id": str(self.id) if self.id else "",
|
|
147
|
+
"name": self.name,
|
|
148
|
+
"description": self.description,
|
|
149
|
+
"params": self.params if isinstance(self.params, dict) else {},
|
|
150
|
+
"aic": self.aic,
|
|
151
|
+
"bic": self.bic,
|
|
152
|
+
"chisqr": self.chisqr,
|
|
153
|
+
"fit_method": self.fit_method,
|
|
154
|
+
"termination_message": self.termination_message,
|
|
155
|
+
"success": self.success,
|
|
156
|
+
"analytical_fast_exchange": self.analytical_fast_exchange,
|
|
157
|
+
"analytical_topology": self.analytical_topology,
|
|
158
|
+
"analytical_obs_columns": list(self.analytical_obs_columns),
|
|
159
|
+
"analytical_obs_components": list(self.analytical_obs_components),
|
|
160
|
+
"analytical_complex_indices": list(self.analytical_complex_indices),
|
|
161
|
+
"fit_speciation": (
|
|
162
|
+
self.fit_speciation.to_dict(orient="list")
|
|
163
|
+
if isinstance(self.fit_speciation, pd.DataFrame)
|
|
164
|
+
else {}
|
|
165
|
+
),
|
|
166
|
+
"calc_obs": (
|
|
167
|
+
self.calc_obs.to_dict(orient="list")
|
|
168
|
+
if isinstance(self.calc_obs, pd.DataFrame)
|
|
169
|
+
else {}
|
|
170
|
+
),
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import logging
|
|
3
|
+
import uuid
|
|
4
|
+
from dataclasses import dataclass, field, InitVar
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
|
|
7
|
+
from nicegui import run
|
|
8
|
+
import numpy as np
|
|
9
|
+
import bindtools.binding as bd
|
|
10
|
+
from functools import partial
|
|
11
|
+
from multiprocessing import Manager, Pool
|
|
12
|
+
|
|
13
|
+
from .ExptData import ExptData
|
|
14
|
+
from .Model import Model
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class MCMCSim:
|
|
20
|
+
"""Data class to represent a MCMC simulation."""
|
|
21
|
+
nwalkers: int = 100
|
|
22
|
+
nsteps_target: int = 1000
|
|
23
|
+
|
|
24
|
+
burn: int = 100
|
|
25
|
+
thin: int = 1
|
|
26
|
+
seed: Optional[int] = None
|
|
27
|
+
chains: np.ndarray = field(
|
|
28
|
+
default_factory=lambda: np.array([])
|
|
29
|
+
) # Array to hold the MCMC chains
|
|
30
|
+
priors: list[dict[str, Any]] = field(default_factory=list)
|
|
31
|
+
model_id: Optional[uuid.UUID] = None
|
|
32
|
+
expt_data_id: Optional[uuid.UUID] = None
|
|
33
|
+
nsteps_done: int = 0
|
|
34
|
+
bd_model: Optional[bd.bindingModel] = None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
model: InitVar[Optional[Model]] = None
|
|
38
|
+
expt_data: InitVar[Optional[ExptData]] = None
|
|
39
|
+
|
|
40
|
+
id: uuid.UUID = field(
|
|
41
|
+
default_factory=lambda: (uuid.uuid4())
|
|
42
|
+
) # unique ID for the instance
|
|
43
|
+
|
|
44
|
+
def __post_init__(self,model,expt_data) -> None:
|
|
45
|
+
"""Ensure data are appropriate types."""
|
|
46
|
+
if not isinstance(self.id, uuid.UUID):
|
|
47
|
+
if isinstance(self.id, str):
|
|
48
|
+
self.id = uuid.UUID(self.id)
|
|
49
|
+
|
|
50
|
+
prior_specs: list[dict[str, Any]] = []
|
|
51
|
+
for prior in self.priors or []:
|
|
52
|
+
if not isinstance(prior, dict):
|
|
53
|
+
continue
|
|
54
|
+
prior_type = str(prior.get("type", "uniform")).lower()
|
|
55
|
+
params = prior.get("params")
|
|
56
|
+
if not isinstance(params, dict):
|
|
57
|
+
params = {
|
|
58
|
+
key: prior.get(key)
|
|
59
|
+
for key in ("lower", "upper", "mu", "sigma")
|
|
60
|
+
if prior.get(key) is not None
|
|
61
|
+
}
|
|
62
|
+
prior_specs.append(
|
|
63
|
+
{
|
|
64
|
+
"label": str(prior.get("label", "")),
|
|
65
|
+
"type": prior_type,
|
|
66
|
+
"params": dict(params),
|
|
67
|
+
}
|
|
68
|
+
)
|
|
69
|
+
self.priors = prior_specs
|
|
70
|
+
|
|
71
|
+
if model is not None:
|
|
72
|
+
self.model_id = model.id
|
|
73
|
+
self._model = model
|
|
74
|
+
else:
|
|
75
|
+
self._model = None
|
|
76
|
+
if expt_data is not None:
|
|
77
|
+
self.expt_data_id = expt_data.id
|
|
78
|
+
self._expt_data = expt_data
|
|
79
|
+
else:
|
|
80
|
+
self._expt_data = None
|
|
81
|
+
|
|
82
|
+
manager = Manager()
|
|
83
|
+
self.cancel_event = manager.Event()
|
|
84
|
+
self.q_percent_done = manager.Queue()
|
|
85
|
+
self.q2_tqdm_out = manager.Queue()
|
|
86
|
+
self.q3_samples = manager.Queue()
|
|
87
|
+
self.chunk_size_val = manager.Value('i', 100)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def find_and_link_expt_data(self, expt_datas: dict[uuid.UUID,ExptData]) -> None:
|
|
91
|
+
"""Link the experimental data to this fit result."""
|
|
92
|
+
if expt_datas is not None:
|
|
93
|
+
if self.expt_data_id in expt_datas and self.expt_data_id is not None:
|
|
94
|
+
self._expt_data = expt_datas[self.expt_data_id]
|
|
95
|
+
return
|
|
96
|
+
else:
|
|
97
|
+
raise ValueError(f"Corresponding experimental data {self.expt_data_id} not found for FitResult.")
|
|
98
|
+
|
|
99
|
+
def find_and_link_model(self, models: dict[uuid.UUID,Model]) -> None:
|
|
100
|
+
"""Link the experimental data to this fit result."""
|
|
101
|
+
if models is not None:
|
|
102
|
+
if self.model_id in models and self.model_id is not None:
|
|
103
|
+
self._model = models[self.model_id]
|
|
104
|
+
return
|
|
105
|
+
else:
|
|
106
|
+
raise ValueError(f"Corresponding model {self.model_id} not found for FitResult.")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def setup(self,obslist: list[bd.ObsType]) -> None:
|
|
111
|
+
if self.bd_model is None:
|
|
112
|
+
raise ValueError("bd_model must be set before running MCMC simulation.")
|
|
113
|
+
if self._expt_data is None:
|
|
114
|
+
raise ValueError("Experimental data must be linked before running MCMC simulation.")
|
|
115
|
+
|
|
116
|
+
self.mc = bd.MCMC(self.bd_model, obslist, walkers=self.nwalkers, samples=self.nsteps_target)
|
|
117
|
+
self._apply_prior_bounds(obslist)
|
|
118
|
+
|
|
119
|
+
def _parameter_specs(self, obslist: list[bd.ObsType]) -> list[dict[str, Any]]:
|
|
120
|
+
if self.bd_model is None or self.bd_model.miniResult is None:
|
|
121
|
+
return []
|
|
122
|
+
|
|
123
|
+
specs: list[dict[str, Any]] = []
|
|
124
|
+
mini_result_params = getattr(self.bd_model.miniResult, "params", None)
|
|
125
|
+
if mini_result_params is None:
|
|
126
|
+
return []
|
|
127
|
+
|
|
128
|
+
for param_name in mini_result_params.keys():
|
|
129
|
+
if not mini_result_params[param_name].vary:
|
|
130
|
+
continue
|
|
131
|
+
param = self.bd_model.params[param_name]
|
|
132
|
+
specs.append(
|
|
133
|
+
{
|
|
134
|
+
"label": str(getattr(param, "name", param_name)),
|
|
135
|
+
"lower": float(param.min),
|
|
136
|
+
"upper": float(param.max),
|
|
137
|
+
}
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
seen_sigma_names: set[str] = set()
|
|
141
|
+
for obs in obslist:
|
|
142
|
+
if obs.name in seen_sigma_names:
|
|
143
|
+
continue
|
|
144
|
+
seen_sigma_names.add(obs.name)
|
|
145
|
+
sigma_param = obs.param
|
|
146
|
+
specs.append(
|
|
147
|
+
{
|
|
148
|
+
"label": str(getattr(sigma_param, "name", obs.name)),
|
|
149
|
+
"lower": float(sigma_param.min),
|
|
150
|
+
"upper": float(sigma_param.max),
|
|
151
|
+
}
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
return specs
|
|
155
|
+
|
|
156
|
+
def _apply_prior_bounds(self, obslist: list[bd.ObsType]) -> None:
|
|
157
|
+
if self.bd_model is None:
|
|
158
|
+
return
|
|
159
|
+
|
|
160
|
+
specs = self._parameter_specs(obslist)
|
|
161
|
+
resolved_bounds: list[list[float]] = []
|
|
162
|
+
for index, spec in enumerate(specs):
|
|
163
|
+
prior = self.priors[index] if index < len(self.priors) else {}
|
|
164
|
+
prior_type = str(prior.get("type", "uniform")).lower()
|
|
165
|
+
params = prior.get("params", {}) if isinstance(prior.get("params", {}), dict) else {}
|
|
166
|
+
lower = params.get("lower", spec["lower"])
|
|
167
|
+
upper = params.get("upper", spec["upper"])
|
|
168
|
+
if prior_type != "uniform":
|
|
169
|
+
if prior_type not in ('none', ''):
|
|
170
|
+
logger.warning(
|
|
171
|
+
"Unsupported prior type '%s' for '%s'; falling back to model bounds.",
|
|
172
|
+
prior_type,
|
|
173
|
+
spec.get("label", f"param_{index}"),
|
|
174
|
+
)
|
|
175
|
+
lower = spec["lower"]
|
|
176
|
+
upper = spec["upper"]
|
|
177
|
+
|
|
178
|
+
try:
|
|
179
|
+
lower_value = float(spec["lower"] if lower is None else lower)
|
|
180
|
+
except (TypeError, ValueError):
|
|
181
|
+
lower_value = float(spec["lower"])
|
|
182
|
+
try:
|
|
183
|
+
upper_value = float(spec["upper"] if upper is None else upper)
|
|
184
|
+
except (TypeError, ValueError):
|
|
185
|
+
upper_value = float(spec["upper"])
|
|
186
|
+
|
|
187
|
+
resolved_bounds.append([lower_value, upper_value])
|
|
188
|
+
|
|
189
|
+
if not hasattr(self.bd_model, "fcn_opts") or self.bd_model.fcn_opts is None:
|
|
190
|
+
self.bd_model.fcn_opts = {}
|
|
191
|
+
self.bd_model.fcn_opts["mcmc_bounds"] = np.array(resolved_bounds, dtype=float)
|
|
192
|
+
|
|
193
|
+
async def run(self,chunk_size: Optional[int]=None) -> None:
|
|
194
|
+
if chunk_size is not None:
|
|
195
|
+
self.chunk_size_val.value = int(chunk_size)
|
|
196
|
+
self.mc = await run.cpu_bound(partial(self._run_mcmc))
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _run_mcmc(self) -> bd.MCMC:
|
|
200
|
+
"""Run the MCMC simulation with multiprocessing in blocks of chunk_size steps."""
|
|
201
|
+
if self.mc is None:
|
|
202
|
+
raise ValueError("MCMC not set up. Call setup() before running the simulation.")
|
|
203
|
+
|
|
204
|
+
with Pool() as pool:
|
|
205
|
+
while self.nsteps_done < self.nsteps_target:
|
|
206
|
+
if self.cancel_event.is_set():
|
|
207
|
+
logger.info('MCMC run cancelled.')
|
|
208
|
+
return self.mc
|
|
209
|
+
chunk_size = self.chunk_size_val.value
|
|
210
|
+
samples = min(chunk_size, self.nsteps_target - self.nsteps_done)
|
|
211
|
+
b = io.StringIO()
|
|
212
|
+
self.mc.run(samples=samples,pool=pool,tqdm_kwargs={'file': b})
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
self.nsteps_done += samples
|
|
217
|
+
self.q_percent_done.put(self.nsteps_done / self.nsteps_target)
|
|
218
|
+
self.q2_tqdm_out.put(b.getvalue().splitlines()[-1])
|
|
219
|
+
if self.mc.sampler is not None:
|
|
220
|
+
a={}
|
|
221
|
+
# a['percent_done'] = self.nsteps_done / self.nsteps_target
|
|
222
|
+
# a['tqdm'] = b.getvalue().splitlines()[-1]
|
|
223
|
+
a['chains'] = self.mc.sampler.get_chain()#discard=self.burn, thin=self.thin, flat=True)
|
|
224
|
+
a['acceptance_fraction'] = self.mc.sampler.acceptance_fraction
|
|
225
|
+
self.q3_samples.put(a)
|
|
226
|
+
logger.info('Completed steps: %s', self.nsteps_done)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
return self.mc
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
from dataclasses import asdict, dataclass, field, InitVar
|
|
3
|
+
from typing import Optional,Any
|
|
4
|
+
import unicodedata
|
|
5
|
+
from nicegui import binding
|
|
6
|
+
import numpy as np
|
|
7
|
+
import pandas as pd
|
|
8
|
+
from lmfit import Parameter as LMFitParameter
|
|
9
|
+
from .Component import Component
|
|
10
|
+
from .BindingConstant import BindingConstant
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class Model:
|
|
14
|
+
"""Data class to represent a model."""
|
|
15
|
+
|
|
16
|
+
name: str = ""
|
|
17
|
+
eq_str: str = ""
|
|
18
|
+
eq_mat_str: str = ""
|
|
19
|
+
eq_mat: np.ndarray = field(
|
|
20
|
+
default_factory=lambda: np.array([])
|
|
21
|
+
) # List of numpy arrays for the equilibrium matrix # TODO this should just be an array
|
|
22
|
+
nComp: int = 2
|
|
23
|
+
nStep: int = 20
|
|
24
|
+
components: list[Component] = field(default_factory=list)
|
|
25
|
+
binding_constants: list[BindingConstant] = field(default_factory=list)
|
|
26
|
+
results: np.ndarray = field(default_factory=lambda: np.array([]))
|
|
27
|
+
species: list[str] = field(default_factory=list)
|
|
28
|
+
component_names: list[str] = field(default_factory=list)
|
|
29
|
+
component_concs: pd.DataFrame = field(
|
|
30
|
+
default_factory=pd.DataFrame, compare=False
|
|
31
|
+
) # compare=False means that __eq__ does not try to do a dataframe comparison, which tends to fail
|
|
32
|
+
id: uuid.UUID = field(
|
|
33
|
+
default_factory=lambda: (uuid.uuid4()))
|
|
34
|
+
|
|
35
|
+
def __post_init__(self):
|
|
36
|
+
"""Ensure data are appropriate types."""
|
|
37
|
+
if not isinstance(self.id, uuid.UUID):
|
|
38
|
+
if isinstance(self.id, str):
|
|
39
|
+
self.id = uuid.UUID(self.id)
|
|
40
|
+
|
|
41
|
+
def to_dict(self):
|
|
42
|
+
"""Convert Model to a dictionary."""
|
|
43
|
+
return {
|
|
44
|
+
"name": self.name,
|
|
45
|
+
"eq_str": self.eq_str,
|
|
46
|
+
"eq_mat_str": str(self.eq_mat_str) if self.eq_mat_str else "",
|
|
47
|
+
"eq_mat": (
|
|
48
|
+
self.eq_mat.tolist() if isinstance(self.eq_mat, np.ndarray) else []
|
|
49
|
+
), # Convert numpy arrays to lists
|
|
50
|
+
"nComp": int(self.nComp),
|
|
51
|
+
"nStep": int(self.nStep),
|
|
52
|
+
"components": (
|
|
53
|
+
[asdict(comp) for comp in self.components]
|
|
54
|
+
if len(self.components) > 0
|
|
55
|
+
else []
|
|
56
|
+
),
|
|
57
|
+
"binding_constants": (
|
|
58
|
+
[asdict(k) for k in self.binding_constants]
|
|
59
|
+
if len(self.binding_constants) > 0
|
|
60
|
+
else []
|
|
61
|
+
),
|
|
62
|
+
"results": self.results.tolist() if hasattr(self,'results') and len(self.results)>0 else [],
|
|
63
|
+
"species": self.species if self.species else [],
|
|
64
|
+
"component_names": self.component_names if self.component_names else [],
|
|
65
|
+
"component_concs": (
|
|
66
|
+
self.component_concs.to_dict()
|
|
67
|
+
if isinstance(self.component_concs, pd.DataFrame)
|
|
68
|
+
else {}
|
|
69
|
+
),
|
|
70
|
+
"id": str(self.id),
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def fullCompSpecList(self) -> list[str]:
|
|
75
|
+
"""Get the full list of component and species names."""
|
|
76
|
+
return [s + "_tot" for s in self.component_names] + [
|
|
77
|
+
s + "_free" for s in self.species
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def __eq__(self,other):
|
|
82
|
+
if not isinstance(other, Model):
|
|
83
|
+
return False
|
|
84
|
+
|
|
85
|
+
return self.id == other.id
|
|
86
|
+
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
from dataclasses import asdict, dataclass, field, InitVar
|
|
3
|
+
from typing import Optional,Any
|
|
4
|
+
import unicodedata
|
|
5
|
+
from nicegui import binding
|
|
6
|
+
import numpy as np
|
|
7
|
+
import pandas as pd
|
|
8
|
+
from lmfit import Parameter as LMFitParameter
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class RawData:
|
|
13
|
+
filename: str = ""
|
|
14
|
+
data: pd.DataFrame = field(default_factory=pd.DataFrame, compare=False)
|
|
15
|
+
id: uuid.UUID = field(default_factory= lambda: uuid.uuid4())
|
|
16
|
+
|
|
17
|
+
def __post_init__(self):
|
|
18
|
+
"""Ensure data are appropriate types."""
|
|
19
|
+
if not isinstance(self.id, uuid.UUID):
|
|
20
|
+
if isinstance(self.id, str):
|
|
21
|
+
self.id = uuid.UUID(self.id)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def to_dict(self) -> dict[str, str|dict]:
|
|
25
|
+
"""Convert RawData to a dictionary."""
|
|
26
|
+
return {
|
|
27
|
+
"filename": self.filename,
|
|
28
|
+
"data": (
|
|
29
|
+
self.data.to_dict(orient="list")
|
|
30
|
+
if isinstance(self.data, pd.DataFrame)
|
|
31
|
+
else {}
|
|
32
|
+
),
|
|
33
|
+
"id": str(self.id) if self.id else "",
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
from dataclasses import asdict, dataclass, field, InitVar
|
|
3
|
+
from typing import Optional,Any
|
|
4
|
+
import unicodedata
|
|
5
|
+
from nicegui import binding
|
|
6
|
+
import numpy as np
|
|
7
|
+
import pandas as pd
|
|
8
|
+
from lmfit import Parameter as LMFitParameter
|
|
9
|
+
from .BindingConstant import BindingConstant
|
|
10
|
+
from .Model import Model
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class Simulation:
|
|
15
|
+
"""Data class to represent a simulation, which comprises:
|
|
16
|
+
- input data (component concentrations or analogous generators)
|
|
17
|
+
- a model (consider separating into a binding model and a data model,
|
|
18
|
+
where the former has parameters of binding constants only, the latter of datafile<=>concentration
|
|
19
|
+
conversions etc.)
|
|
20
|
+
- params (comprising at minimum a list of binding constants)
|
|
21
|
+
- output data (speies concentrations)"""
|
|
22
|
+
|
|
23
|
+
comp_concs: pd.DataFrame = field(
|
|
24
|
+
default_factory=pd.DataFrame, compare=False
|
|
25
|
+
) # compare=False means that __eq__ does not try to do a dataframe comparison, which tends to fail
|
|
26
|
+
model_id: uuid.UUID | None = None # The modelUID used for the simulation
|
|
27
|
+
params: list[BindingConstant] = field(
|
|
28
|
+
default_factory=list
|
|
29
|
+
) # List of binding constants and other parameters
|
|
30
|
+
results: pd.DataFrame = field(
|
|
31
|
+
default_factory=pd.DataFrame
|
|
32
|
+
) # DataFrame to hold output species concentrations
|
|
33
|
+
id: uuid.UUID = field(
|
|
34
|
+
default_factory=lambda: (uuid.uuid4())
|
|
35
|
+
) # unique ID for the instance
|
|
36
|
+
comment: str = "" # Optional comment for the simulation
|
|
37
|
+
name: str = ""
|
|
38
|
+
|
|
39
|
+
def __post_init__(self):
|
|
40
|
+
"""Ensure data are appropriate types."""
|
|
41
|
+
if not isinstance(self.id, uuid.UUID):
|
|
42
|
+
if isinstance(self.id, str):
|
|
43
|
+
self.id = uuid.UUID(self.id)
|
|
44
|
+
|
|
45
|
+
if not isinstance(self.model_id,uuid.UUID):
|
|
46
|
+
if isinstance(self.model_id, str):
|
|
47
|
+
self.model_id = uuid.UUID(self.model_id)
|
|
48
|
+
|
|
49
|
+
# deep copy dataframes to avoid issues with mutability
|
|
50
|
+
if isinstance(self.comp_concs, pd.DataFrame):
|
|
51
|
+
self.comp_concs = self.comp_concs.copy()
|
|
52
|
+
if isinstance(self.results, pd.DataFrame):
|
|
53
|
+
self.results = self.results.copy()
|
|
54
|
+
|
|
55
|
+
def to_dict(self) -> dict[str, str|dict|list]:
|
|
56
|
+
return {
|
|
57
|
+
"comp_concs": (
|
|
58
|
+
self.comp_concs.to_dict()
|
|
59
|
+
if isinstance(self.comp_concs, pd.DataFrame)
|
|
60
|
+
else {}
|
|
61
|
+
),
|
|
62
|
+
"model_id": str(self.model_id) if self.model_id else "",
|
|
63
|
+
"params": [asdict(k) for k in self.params] if len(self.params) > 0 else [],
|
|
64
|
+
"results": (
|
|
65
|
+
self.results.to_dict(orient="list")
|
|
66
|
+
if isinstance(self.results, pd.DataFrame)
|
|
67
|
+
else {}
|
|
68
|
+
),
|
|
69
|
+
"id": str(self.id),
|
|
70
|
+
"comment": self.comment,
|
|
71
|
+
"name": self.name,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def model(self) -> Optional[Model]:
|
|
76
|
+
"""Get the model associated with this simulation."""
|
|
77
|
+
return self._model if hasattr(self, "_model") else None
|
|
78
|
+
|
|
79
|
+
@model.setter
|
|
80
|
+
def model(self, model: Model) -> None:
|
|
81
|
+
"""Set the model for this simulation."""
|
|
82
|
+
if model is not None:
|
|
83
|
+
self.model_id = model.id
|
|
84
|
+
self._model = model
|
|
85
|
+
else:
|
|
86
|
+
self.model_id = None
|
|
87
|
+
self._model = None
|
|
88
|
+
|
|
89
|
+
def find_and_link_model(self, models: Optional[dict[uuid.UUID,Model]] = None) -> None:
|
|
90
|
+
"""Set the model for this simulation."""
|
|
91
|
+
if models is not None:
|
|
92
|
+
if self.model_id in models and self.model_id is not None:
|
|
93
|
+
self._model = models[self.model_id]
|
|
94
|
+
else:
|
|
95
|
+
raise ValueError(f"Corresponding model {self.model_id} not found for Simulation.")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def __eq__(self,other):
|
|
100
|
+
if not isinstance(other, Simulation):
|
|
101
|
+
return False
|
|
102
|
+
|
|
103
|
+
return self.id == other.id
|
|
104
|
+
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
from dataclasses import asdict, dataclass, field, InitVar
|
|
3
|
+
from typing import Optional,Any
|
|
4
|
+
import unicodedata
|
|
5
|
+
from nicegui import binding
|
|
6
|
+
import numpy as np
|
|
7
|
+
import pandas as pd
|
|
8
|
+
from lmfit import Parameter as LMFitParameter
|
|
9
|
+
|
|
10
|
+
@binding.bindable_dataclass
|
|
11
|
+
@dataclass
|
|
12
|
+
class UIBindings:
|
|
13
|
+
"""Helper class to hold UI bindings."""
|
|
14
|
+
model_name: str = ""
|
|
15
|
+
raw_data_name: str = ""
|
|
16
|
+
data_model_name: str = ""
|
|
17
|
+
fit_name: str = ""
|
|
18
|
+
sim_name: str = ""
|
|
19
|
+
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# webgui/classes/__init__.py
|
|
2
|
+
from .BindingConstant import BindingConstant
|
|
3
|
+
from .Component import Component
|
|
4
|
+
from .Model import Model
|
|
5
|
+
from .RawData import RawData
|
|
6
|
+
from .ChemicalShiftParam import ChemicalShiftParam
|
|
7
|
+
from .ExptData import ExptData
|
|
8
|
+
from .ExptDataType import ExptDataType
|
|
9
|
+
from .FitResult import FitResult
|
|
10
|
+
from .Simulation import Simulation
|
|
11
|
+
from .MCMCSim import MCMCSim
|
|
12
|
+
from .UIBindings import UIBindings
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"BindingConstant",
|
|
17
|
+
"Component",
|
|
18
|
+
"Model",
|
|
19
|
+
"RawData",
|
|
20
|
+
"ChemicalShiftParam",
|
|
21
|
+
"ExptData",
|
|
22
|
+
"FitResult",
|
|
23
|
+
"Simulation",
|
|
24
|
+
"MCMCSim",
|
|
25
|
+
"ExptDataType",
|
|
26
|
+
"UIBindings"
|
|
27
|
+
|
|
28
|
+
]
|