bindmc 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. bindmc/main.py +67 -0
  2. bindmc/webgui/__init__.py +0 -0
  3. bindmc/webgui/app.py +54 -0
  4. bindmc/webgui/classes/BindingConstant.py +23 -0
  5. bindmc/webgui/classes/ChemicalShiftParam.py +40 -0
  6. bindmc/webgui/classes/Component.py +111 -0
  7. bindmc/webgui/classes/ExptData.py +485 -0
  8. bindmc/webgui/classes/ExptDataType.py +92 -0
  9. bindmc/webgui/classes/FitResult.py +173 -0
  10. bindmc/webgui/classes/MCMCSim.py +232 -0
  11. bindmc/webgui/classes/Model.py +86 -0
  12. bindmc/webgui/classes/RawData.py +36 -0
  13. bindmc/webgui/classes/Simulation.py +104 -0
  14. bindmc/webgui/classes/UIBindings.py +19 -0
  15. bindmc/webgui/classes/__init__.py +28 -0
  16. bindmc/webgui/components/__init__.py +29 -0
  17. bindmc/webgui/components/base.py +24 -0
  18. bindmc/webgui/components/bayes.py +689 -0
  19. bindmc/webgui/components/bayes_priors.py +351 -0
  20. bindmc/webgui/components/binding_model.py +330 -0
  21. bindmc/webgui/components/body.py +276 -0
  22. bindmc/webgui/components/data_gen.py +419 -0
  23. bindmc/webgui/components/data_import.py +450 -0
  24. bindmc/webgui/components/data_model.py +609 -0
  25. bindmc/webgui/components/fitting.py +886 -0
  26. bindmc/webgui/components/graph.py +649 -0
  27. bindmc/webgui/components/header.py +124 -0
  28. bindmc/webgui/components/simulation.py +385 -0
  29. bindmc/webgui/export/__init__.py +0 -0
  30. bindmc/webgui/export/notebook_exporter.py +727 -0
  31. bindmc/webgui/state/__init__.py +1 -0
  32. bindmc/webgui/state/statemanager.py +2043 -0
  33. bindmc/webgui/utils.py +322 -0
  34. bindmc-0.1.0.dist-info/METADATA +22 -0
  35. bindmc-0.1.0.dist-info/RECORD +37 -0
  36. bindmc-0.1.0.dist-info/WHEEL +5 -0
  37. bindmc-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,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
+ ]