AMS-BP 0.0.2__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.
- AMS_BP/__init__.py +13 -0
- AMS_BP/cells/__init__.py +5 -0
- AMS_BP/cells/base_cell.py +55 -0
- AMS_BP/cells/rectangular_cell.py +82 -0
- AMS_BP/cells/rod_cell.py +98 -0
- AMS_BP/cells/spherical_cell.py +74 -0
- AMS_BP/configio/__init__.py +0 -0
- AMS_BP/configio/configmodels.py +93 -0
- AMS_BP/configio/convertconfig.py +910 -0
- AMS_BP/configio/experiments.py +121 -0
- AMS_BP/configio/saving.py +32 -0
- AMS_BP/metadata/__init__.py +0 -0
- AMS_BP/metadata/metadata.py +87 -0
- AMS_BP/motion/__init__.py +4 -0
- AMS_BP/motion/condensate_movement.py +356 -0
- AMS_BP/motion/movement/__init__.py +10 -0
- AMS_BP/motion/movement/boundary_conditions.py +75 -0
- AMS_BP/motion/movement/fbm_BP.py +244 -0
- AMS_BP/motion/track_gen.py +541 -0
- AMS_BP/optics/__init__.py +0 -0
- AMS_BP/optics/camera/__init__.py +4 -0
- AMS_BP/optics/camera/detectors.py +320 -0
- AMS_BP/optics/camera/quantum_eff.py +66 -0
- AMS_BP/optics/filters/__init__.py +17 -0
- AMS_BP/optics/filters/channels/__init__.py +0 -0
- AMS_BP/optics/filters/channels/channelschema.py +27 -0
- AMS_BP/optics/filters/filters.py +184 -0
- AMS_BP/optics/lasers/__init__.py +28 -0
- AMS_BP/optics/lasers/laser_profiles.py +691 -0
- AMS_BP/optics/psf/__init__.py +7 -0
- AMS_BP/optics/psf/psf_engine.py +215 -0
- AMS_BP/photophysics/__init__.py +0 -0
- AMS_BP/photophysics/photon_physics.py +181 -0
- AMS_BP/photophysics/state_kinetics.py +146 -0
- AMS_BP/probabilityfuncs/__init__.py +0 -0
- AMS_BP/probabilityfuncs/markov_chain.py +143 -0
- AMS_BP/probabilityfuncs/probability_functions.py +350 -0
- AMS_BP/run_cell_simulation.py +217 -0
- AMS_BP/sample/__init__.py +0 -0
- AMS_BP/sample/flurophores/__init__.py +16 -0
- AMS_BP/sample/flurophores/flurophore_schema.py +290 -0
- AMS_BP/sample/sim_sampleplane.py +334 -0
- AMS_BP/sim_config.toml +418 -0
- AMS_BP/sim_microscopy.py +453 -0
- AMS_BP/utils/__init__.py +0 -0
- AMS_BP/utils/constants.py +11 -0
- AMS_BP/utils/decorators.py +227 -0
- AMS_BP/utils/errors.py +37 -0
- AMS_BP/utils/maskMaker.py +12 -0
- AMS_BP/utils/util_functions.py +319 -0
- ams_bp-0.0.2.dist-info/METADATA +173 -0
- ams_bp-0.0.2.dist-info/RECORD +55 -0
- ams_bp-0.0.2.dist-info/WHEEL +4 -0
- ams_bp-0.0.2.dist-info/entry_points.txt +2 -0
- ams_bp-0.0.2.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,910 @@
|
|
1
|
+
from copy import deepcopy
|
2
|
+
from pathlib import Path
|
3
|
+
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
|
4
|
+
|
5
|
+
import tomli
|
6
|
+
from pydantic import BaseModel
|
7
|
+
|
8
|
+
from ..cells import RectangularCell
|
9
|
+
from ..cells.base_cell import BaseCell
|
10
|
+
from ..motion import Track_generator, create_condensate_dict
|
11
|
+
from ..motion.track_gen import (
|
12
|
+
_convert_tracks_to_trajectory,
|
13
|
+
_generate_constant_tracks,
|
14
|
+
_generate_no_transition_tracks,
|
15
|
+
_generate_transition_tracks,
|
16
|
+
)
|
17
|
+
from ..optics.camera.detectors import CMOSDetector, Detector, EMCCDDetector
|
18
|
+
from ..optics.camera.quantum_eff import QuantumEfficiency
|
19
|
+
from ..optics.filters import (
|
20
|
+
FilterSet,
|
21
|
+
FilterSpectrum,
|
22
|
+
create_allow_all_filter,
|
23
|
+
create_bandpass_filter,
|
24
|
+
create_tophat_filter,
|
25
|
+
)
|
26
|
+
from ..optics.filters.channels.channelschema import Channels
|
27
|
+
from ..optics.lasers.laser_profiles import (
|
28
|
+
GaussianBeam,
|
29
|
+
HiLoBeam,
|
30
|
+
LaserParameters,
|
31
|
+
LaserProfile,
|
32
|
+
WidefieldBeam,
|
33
|
+
)
|
34
|
+
from ..optics.psf.psf_engine import PSFEngine, PSFParameters
|
35
|
+
from ..probabilityfuncs.markov_chain import change_prob_time
|
36
|
+
from ..probabilityfuncs.probability_functions import (
|
37
|
+
generate_points_from_cls as gen_points,
|
38
|
+
)
|
39
|
+
from ..probabilityfuncs.probability_functions import multiple_top_hat_probability as tp
|
40
|
+
from ..sample.flurophores.flurophore_schema import (
|
41
|
+
Fluorophore,
|
42
|
+
SpectralData,
|
43
|
+
State,
|
44
|
+
StateTransition,
|
45
|
+
StateType,
|
46
|
+
)
|
47
|
+
from ..sample.sim_sampleplane import SamplePlane, SampleSpace
|
48
|
+
from ..sim_microscopy import VirtualMicroscope
|
49
|
+
from .configmodels import (
|
50
|
+
CellParameters,
|
51
|
+
CondensateParameters,
|
52
|
+
ConfigList,
|
53
|
+
GlobalParameters,
|
54
|
+
MoleculeParameters,
|
55
|
+
OutputParameters,
|
56
|
+
)
|
57
|
+
from .experiments import (
|
58
|
+
BaseExpConfig,
|
59
|
+
TimeSeriesExpConfig,
|
60
|
+
timeseriesEXP,
|
61
|
+
zseriesEXP,
|
62
|
+
zStackExpConfig,
|
63
|
+
)
|
64
|
+
|
65
|
+
FILTERSET_BASE = ["excitation", "emission", "dichroic"]
|
66
|
+
|
67
|
+
|
68
|
+
def load_config(config_path: Union[str, Path]) -> Dict[str, Any]:
|
69
|
+
"""
|
70
|
+
Load and parse a TOML configuration file.
|
71
|
+
|
72
|
+
Args:
|
73
|
+
config_path: Path to the TOML configuration file (can be string or Path object)
|
74
|
+
|
75
|
+
Returns:
|
76
|
+
Dict[str, Any]: Parsed configuration dictionary
|
77
|
+
|
78
|
+
Raises:
|
79
|
+
FileNotFoundError: If the config file doesn't exist
|
80
|
+
tomli.TOMLDecodeError: If the TOML file is invalid
|
81
|
+
"""
|
82
|
+
# Convert string path to Path object if necessary
|
83
|
+
path = Path(config_path) if isinstance(config_path, str) else config_path
|
84
|
+
|
85
|
+
# Check if file exists
|
86
|
+
if not path.exists():
|
87
|
+
raise FileNotFoundError(f"Configuration file not found: {path}")
|
88
|
+
|
89
|
+
# Load and parse TOML file
|
90
|
+
try:
|
91
|
+
with open(path, "rb") as f:
|
92
|
+
return tomli.load(f)
|
93
|
+
except tomli.TOMLDecodeError as e:
|
94
|
+
raise tomli.TOMLDecodeError(f"Error parsing TOML file {path}: {str(e)}")
|
95
|
+
|
96
|
+
|
97
|
+
class ConfigLoader:
|
98
|
+
def __init__(self, config_path: Union[str, Path, dict]):
|
99
|
+
# if exists, load config, otherwise raise error
|
100
|
+
if isinstance(config_path, dict):
|
101
|
+
self.config = config_path
|
102
|
+
elif not Path(config_path).exists():
|
103
|
+
print(f"Configuration file not found: {config_path}")
|
104
|
+
self.config_path = None
|
105
|
+
else:
|
106
|
+
self.config_path = config_path
|
107
|
+
self.config = load_config(config_path)
|
108
|
+
|
109
|
+
def _reload_config(self):
|
110
|
+
if self.config_path is not None:
|
111
|
+
self.config = load_config(config_path=self.config_path)
|
112
|
+
|
113
|
+
def create_dataclass_schema(
|
114
|
+
self, dataclass_schema: type[BaseModel], config: Dict[str, Any]
|
115
|
+
) -> BaseModel:
|
116
|
+
"""
|
117
|
+
Populate a dataclass schema with configuration data.
|
118
|
+
"""
|
119
|
+
return dataclass_schema(**config)
|
120
|
+
|
121
|
+
def populate_dataclass_schema(self) -> None:
|
122
|
+
"""
|
123
|
+
Populate a dataclass schema with configuration data.
|
124
|
+
"""
|
125
|
+
self.global_params = self.create_dataclass_schema(
|
126
|
+
GlobalParameters, self.config["Global_Parameters"]
|
127
|
+
)
|
128
|
+
self.cell_params = self.create_dataclass_schema(
|
129
|
+
CellParameters, self.config["Cell_Parameters"]
|
130
|
+
)
|
131
|
+
self.molecule_params = self.create_dataclass_schema(
|
132
|
+
MoleculeParameters, self.config["Molecule_Parameters"]
|
133
|
+
)
|
134
|
+
self.condensate_params = self.create_dataclass_schema(
|
135
|
+
CondensateParameters, self.config["Condensate_Parameters"]
|
136
|
+
)
|
137
|
+
self.output_params = self.create_dataclass_schema(
|
138
|
+
OutputParameters, self.config["Output_Parameters"]
|
139
|
+
)
|
140
|
+
|
141
|
+
def create_experiment_from_config(
|
142
|
+
self, config: Dict[str, Any]
|
143
|
+
) -> Tuple[BaseExpConfig, Callable]:
|
144
|
+
configEXP = deepcopy(config["experiment"])
|
145
|
+
if configEXP.get("experiment_type") == "time-series":
|
146
|
+
del configEXP["experiment_type"]
|
147
|
+
tconfig = TimeSeriesExpConfig(**configEXP)
|
148
|
+
callableEXP = timeseriesEXP
|
149
|
+
elif configEXP.get("experiment_type") == "z-stack":
|
150
|
+
del configEXP["experiment_type"]
|
151
|
+
tconfig = zStackExpConfig(**configEXP)
|
152
|
+
callableEXP = zseriesEXP
|
153
|
+
else:
|
154
|
+
raise TypeError("Experiment is not supported")
|
155
|
+
return tconfig, callableEXP
|
156
|
+
|
157
|
+
def create_fluorophores_from_config(
|
158
|
+
self, config: Dict[str, Any]
|
159
|
+
) -> List[Fluorophore]:
|
160
|
+
# Extract fluorophore section
|
161
|
+
fluor_config = config.get("fluorophores", {})
|
162
|
+
if not fluor_config:
|
163
|
+
raise ValueError("No fluorophores configuration found in config")
|
164
|
+
num_fluorophores = fluor_config["num_of_fluorophores"]
|
165
|
+
fluorophore_names = fluor_config["fluorophore_names"]
|
166
|
+
fluorophores = []
|
167
|
+
for i in range(num_fluorophores):
|
168
|
+
fluorophores.append(
|
169
|
+
self.create_fluorophore_from_config(fluor_config[fluorophore_names[i]])
|
170
|
+
)
|
171
|
+
return fluorophores
|
172
|
+
|
173
|
+
def create_fluorophore_from_config(self, config: Dict[str, Any]) -> Fluorophore:
|
174
|
+
"""
|
175
|
+
Create a fluorophore instance from a configuration dictionary.
|
176
|
+
|
177
|
+
Args:
|
178
|
+
config: Dictionary containing the full configuration (typically loaded from TOML)
|
179
|
+
|
180
|
+
Returns:
|
181
|
+
Fluorophore: A Fluorophore instance with the loaded configuration
|
182
|
+
"""
|
183
|
+
# Extract fluorophore section
|
184
|
+
fluor_config = config
|
185
|
+
if not fluor_config:
|
186
|
+
raise ValueError("No fluorophore configuration found.")
|
187
|
+
|
188
|
+
# Build states
|
189
|
+
states = {}
|
190
|
+
for state_name, state_data in fluor_config.get("states", {}).items():
|
191
|
+
# Create spectral data if present
|
192
|
+
excitation_spectrum = (
|
193
|
+
SpectralData(
|
194
|
+
wavelengths=state_data.get("excitation_spectrum", {}).get(
|
195
|
+
"wavelengths", []
|
196
|
+
),
|
197
|
+
intensities=state_data.get("excitation_spectrum", {}).get(
|
198
|
+
"intensities", []
|
199
|
+
),
|
200
|
+
)
|
201
|
+
if "excitation_spectrum" in state_data
|
202
|
+
else None
|
203
|
+
)
|
204
|
+
|
205
|
+
emission_spectrum = (
|
206
|
+
SpectralData(
|
207
|
+
wavelengths=state_data.get("emission_spectrum", {}).get(
|
208
|
+
"wavelengths", []
|
209
|
+
),
|
210
|
+
intensities=state_data.get("emission_spectrum", {}).get(
|
211
|
+
"intensities", []
|
212
|
+
),
|
213
|
+
)
|
214
|
+
if "emission_spectrum" in state_data
|
215
|
+
else None
|
216
|
+
)
|
217
|
+
|
218
|
+
extinction_coefficient = state_data.get("extinction_coefficient")
|
219
|
+
quantum_yield = state_data.get("quantum_yield")
|
220
|
+
molar_cross_section = state_data.get("molar_cross_section")
|
221
|
+
fluorescent_lifetime = state_data.get("fluorescent_lifetime")
|
222
|
+
|
223
|
+
# Create state
|
224
|
+
state = State(
|
225
|
+
name=state_data["name"],
|
226
|
+
state_type=StateType(state_data["state_type"]),
|
227
|
+
excitation_spectrum=excitation_spectrum,
|
228
|
+
emission_spectrum=emission_spectrum,
|
229
|
+
quantum_yield_lambda_val=quantum_yield,
|
230
|
+
extinction_coefficient_lambda_val=extinction_coefficient,
|
231
|
+
molar_cross_section=molar_cross_section,
|
232
|
+
quantum_yield=None,
|
233
|
+
extinction_coefficient=None,
|
234
|
+
fluorescent_lifetime=fluorescent_lifetime,
|
235
|
+
)
|
236
|
+
states[state.name] = state
|
237
|
+
|
238
|
+
initial_state = None
|
239
|
+
state_list = []
|
240
|
+
for state in states.values():
|
241
|
+
state_list.append(state.name)
|
242
|
+
if state.name == fluor_config["initial_state"]:
|
243
|
+
initial_state = state
|
244
|
+
|
245
|
+
if initial_state is None:
|
246
|
+
raise ValueError(
|
247
|
+
f"Inital state must be a valid name from the provided states: {state_list}."
|
248
|
+
)
|
249
|
+
|
250
|
+
# Build transitions
|
251
|
+
transitions = {}
|
252
|
+
for _, trans_data in fluor_config.get("transitions", {}).items():
|
253
|
+
if trans_data.get("photon_dependent", False):
|
254
|
+
transition = StateTransition(
|
255
|
+
from_state=trans_data["from_state"],
|
256
|
+
to_state=trans_data["to_state"],
|
257
|
+
spectrum=SpectralData(
|
258
|
+
wavelengths=trans_data.get("spectrum")["wavelengths"],
|
259
|
+
intensities=trans_data.get("spectrum")["intensities"],
|
260
|
+
),
|
261
|
+
extinction_coefficient_lambda_val=trans_data.get("spectrum")[
|
262
|
+
"extinction_coefficient"
|
263
|
+
],
|
264
|
+
extinction_coefficient=None,
|
265
|
+
cross_section=None,
|
266
|
+
base_rate=None,
|
267
|
+
quantum_yield=trans_data.get("spectrum")["quantum_yield"],
|
268
|
+
)
|
269
|
+
else:
|
270
|
+
transition = StateTransition(
|
271
|
+
from_state=trans_data["from_state"],
|
272
|
+
to_state=trans_data["to_state"],
|
273
|
+
base_rate=trans_data.get("base_rate", None),
|
274
|
+
spectrum=None,
|
275
|
+
extinction_coefficient_lambda_val=None,
|
276
|
+
extinction_coefficient=None,
|
277
|
+
cross_section=None,
|
278
|
+
quantum_yield=None,
|
279
|
+
)
|
280
|
+
transitions[transition.from_state + transition.to_state] = transition
|
281
|
+
|
282
|
+
# Create and return fluorophore
|
283
|
+
return Fluorophore(
|
284
|
+
name=fluor_config["name"],
|
285
|
+
states=states,
|
286
|
+
transitions=transitions,
|
287
|
+
initial_state=initial_state,
|
288
|
+
)
|
289
|
+
|
290
|
+
def create_psf_from_config(
|
291
|
+
self, config: Dict[str, Any]
|
292
|
+
) -> Tuple[Callable, Dict[str, Any]]:
|
293
|
+
"""
|
294
|
+
Create a PSF engine instance from a configuration dictionary.
|
295
|
+
|
296
|
+
Args:
|
297
|
+
config: Dictionary containing the full configuration (typically loaded from TOML)
|
298
|
+
|
299
|
+
Returns:
|
300
|
+
Tuple[Callable, Optional[Dict]]: A tuple containing:
|
301
|
+
- Partial_PSFEngine partial funcion of PSFEngine. Called as f(wavelength, z_step)
|
302
|
+
- Parameters:
|
303
|
+
- wavelength (int, float) in nm
|
304
|
+
- wavelength of the emitted light from the sample after emission filters
|
305
|
+
- z_step (int, float) in um
|
306
|
+
- z_step used to parameterize the psf grid.
|
307
|
+
- Additional PSF-specific parameters (like custom path if specified)
|
308
|
+
"""
|
309
|
+
# Extract PSF section
|
310
|
+
psf_config = config.get("psf", {})
|
311
|
+
if not psf_config:
|
312
|
+
raise ValueError("No PSF configuration found in config")
|
313
|
+
|
314
|
+
# Extract parameters section
|
315
|
+
params_config = psf_config.get("parameters", {})
|
316
|
+
if not params_config:
|
317
|
+
raise ValueError("No PSF parameters found in config")
|
318
|
+
pixel_size = self._find_pixel_size(
|
319
|
+
config["camera"]["magnification"], config["camera"]["pixel_detector_size"]
|
320
|
+
)
|
321
|
+
|
322
|
+
def Partial_PSFengine(
|
323
|
+
wavelength: int | float, z_step: Optional[int | float] = None
|
324
|
+
):
|
325
|
+
# Create PSFParameters instance
|
326
|
+
parameters = PSFParameters(
|
327
|
+
wavelength=wavelength,
|
328
|
+
numerical_aperture=float(params_config["numerical_aperture"]),
|
329
|
+
pixel_size=pixel_size,
|
330
|
+
z_step=float(params_config["z_step"]) if z_step is None else z_step,
|
331
|
+
refractive_index=float(params_config.get("refractive_index", 1.0)),
|
332
|
+
)
|
333
|
+
|
334
|
+
# Create PSF engine
|
335
|
+
psf_engine = PSFEngine(parameters)
|
336
|
+
return psf_engine
|
337
|
+
|
338
|
+
# Extract additional configuration
|
339
|
+
additional_config = {
|
340
|
+
"type": psf_config.get("type", "gaussian"),
|
341
|
+
"custom_path": psf_config.get("custom_path", ""),
|
342
|
+
}
|
343
|
+
|
344
|
+
return Partial_PSFengine, additional_config
|
345
|
+
|
346
|
+
@staticmethod
|
347
|
+
def _find_pixel_size(magnification: float, pixel_detector_size: float) -> float:
|
348
|
+
return pixel_detector_size / magnification
|
349
|
+
|
350
|
+
def create_laser_from_config(
|
351
|
+
self, laser_config: Dict[str, Any], preset: str
|
352
|
+
) -> LaserProfile:
|
353
|
+
"""
|
354
|
+
Create a laser profile instance from a configuration dictionary.
|
355
|
+
|
356
|
+
Args:
|
357
|
+
laser_config: Dictionary containing the laser configuration
|
358
|
+
preset: Name of the laser preset (e.g., 'blue', 'green', 'red')
|
359
|
+
|
360
|
+
Returns:
|
361
|
+
LaserProfile: A LaserProfile instance with the loaded configuration
|
362
|
+
"""
|
363
|
+
# Extract laser parameters
|
364
|
+
params_config = laser_config.get("parameters", {})
|
365
|
+
if not params_config:
|
366
|
+
raise ValueError(f"No parameters found for laser: {preset}")
|
367
|
+
|
368
|
+
# Create LaserParameters instance
|
369
|
+
parameters = LaserParameters(
|
370
|
+
power=float(params_config["power"]),
|
371
|
+
wavelength=float(params_config["wavelength"]),
|
372
|
+
beam_width=float(params_config["beam_width"]),
|
373
|
+
numerical_aperture=float(params_config.get("numerical_aperture")),
|
374
|
+
refractive_index=float(params_config.get("refractive_index", 1.0)),
|
375
|
+
)
|
376
|
+
|
377
|
+
# Create appropriate laser profile based on type
|
378
|
+
laser_type = laser_config.get("type", "gaussian").lower()
|
379
|
+
|
380
|
+
if laser_type == "gaussian":
|
381
|
+
return GaussianBeam(parameters)
|
382
|
+
if laser_type == "widefield":
|
383
|
+
return WidefieldBeam(parameters)
|
384
|
+
if laser_type == "hilo":
|
385
|
+
try:
|
386
|
+
params_config.get("inclination_angle")
|
387
|
+
except KeyError:
|
388
|
+
raise KeyError("HiLo needs inclination angle. Currently not provided")
|
389
|
+
return HiLoBeam(parameters, float(params_config["inclination_angle"]))
|
390
|
+
else:
|
391
|
+
raise ValueError(f"Unknown laser type: {laser_type}")
|
392
|
+
|
393
|
+
def create_lasers_from_config(
|
394
|
+
self, config: Dict[str, Any]
|
395
|
+
) -> Dict[str, LaserProfile]:
|
396
|
+
"""
|
397
|
+
Create multiple laser profile instances from a configuration dictionary.
|
398
|
+
|
399
|
+
Args:
|
400
|
+
config: Dictionary containing the full configuration (typically loaded from TOML)
|
401
|
+
|
402
|
+
Returns:
|
403
|
+
Dict[str, LaserProfile]: Dictionary mapping laser names to their profile instances
|
404
|
+
"""
|
405
|
+
# Extract lasers section
|
406
|
+
lasers_config = config.get("lasers", {})
|
407
|
+
if not lasers_config:
|
408
|
+
raise ValueError("No lasers configuration found in config")
|
409
|
+
|
410
|
+
# Get active lasers
|
411
|
+
active_lasers = lasers_config.get("active", [])
|
412
|
+
if not active_lasers:
|
413
|
+
raise ValueError("No active lasers specified in configuration")
|
414
|
+
|
415
|
+
# Create laser profiles for each active laser
|
416
|
+
laser_profiles = {}
|
417
|
+
for laser_name in active_lasers:
|
418
|
+
laser_config = lasers_config.get(laser_name)
|
419
|
+
if not laser_config:
|
420
|
+
raise ValueError(f"Configuration not found for laser: {laser_name}")
|
421
|
+
|
422
|
+
laser_profiles[laser_name] = self.create_laser_from_config(
|
423
|
+
laser_config, laser_name
|
424
|
+
)
|
425
|
+
|
426
|
+
return laser_profiles
|
427
|
+
|
428
|
+
def create_filter_spectrum_from_config(
|
429
|
+
self, filter_config: Dict[str, Any]
|
430
|
+
) -> FilterSpectrum:
|
431
|
+
"""
|
432
|
+
Create a filter spectrum from configuration dictionary.
|
433
|
+
|
434
|
+
Args:
|
435
|
+
filter_config: Dictionary containing filter configuration
|
436
|
+
|
437
|
+
Returns:
|
438
|
+
FilterSpectrum: Created filter spectrum instance
|
439
|
+
"""
|
440
|
+
filter_type = filter_config.get("type", "").lower()
|
441
|
+
|
442
|
+
if filter_type == "bandpass":
|
443
|
+
return create_bandpass_filter(
|
444
|
+
center_wavelength=float(filter_config["center_wavelength"]),
|
445
|
+
bandwidth=float(filter_config["bandwidth"]),
|
446
|
+
transmission_peak=float(filter_config.get("transmission_peak", 0.95)),
|
447
|
+
points=int(filter_config.get("points", 1000)),
|
448
|
+
name=filter_config.get("name"),
|
449
|
+
)
|
450
|
+
elif filter_type == "tophat":
|
451
|
+
return create_tophat_filter(
|
452
|
+
center_wavelength=float(filter_config["center_wavelength"]),
|
453
|
+
bandwidth=float(filter_config["bandwidth"]),
|
454
|
+
transmission_peak=float(filter_config.get("transmission_peak", 0.95)),
|
455
|
+
edge_steepness=float(filter_config.get("edge_steepness", 5.0)),
|
456
|
+
points=int(filter_config.get("points", 1000)),
|
457
|
+
name=filter_config.get("name"),
|
458
|
+
)
|
459
|
+
elif filter_type == "allow_all":
|
460
|
+
return create_allow_all_filter(
|
461
|
+
points=int(filter_config.get("points", 1000)),
|
462
|
+
name=filter_config.get("name"),
|
463
|
+
)
|
464
|
+
|
465
|
+
else:
|
466
|
+
raise ValueError(f"Unsupported filter type: {filter_type}")
|
467
|
+
|
468
|
+
def create_filter_set_from_config(self, config: Dict[str, Any]) -> FilterSet:
|
469
|
+
"""
|
470
|
+
Create a filter set from configuration dictionary.
|
471
|
+
|
472
|
+
Args:
|
473
|
+
config: Dictionary containing the full configuration (typically loaded from TOML)
|
474
|
+
|
475
|
+
Returns:
|
476
|
+
FilterSet: Created filter set instance
|
477
|
+
"""
|
478
|
+
# Extract filters section
|
479
|
+
filters_config = config
|
480
|
+
if not filters_config:
|
481
|
+
raise ValueError("No filters configuration found in config")
|
482
|
+
|
483
|
+
missing = []
|
484
|
+
for base_filter in FILTERSET_BASE:
|
485
|
+
if base_filter not in filters_config:
|
486
|
+
print(f"Missing {base_filter} filter in filter set; using base config")
|
487
|
+
missing.append(base_filter)
|
488
|
+
|
489
|
+
if missing:
|
490
|
+
for base_filter in missing:
|
491
|
+
filters_config[base_filter] = {
|
492
|
+
"type": "allow_all",
|
493
|
+
"points": 1000,
|
494
|
+
"name": f"{base_filter} filter",
|
495
|
+
}
|
496
|
+
|
497
|
+
# Create filter components
|
498
|
+
excitation = self.create_filter_spectrum_from_config(
|
499
|
+
filters_config["excitation"]
|
500
|
+
)
|
501
|
+
emission = self.create_filter_spectrum_from_config(filters_config["emission"])
|
502
|
+
dichroic = self.create_filter_spectrum_from_config(filters_config["dichroic"])
|
503
|
+
|
504
|
+
# Create filter set
|
505
|
+
return FilterSet(
|
506
|
+
name=filters_config.get("filter_set_name", "Custom Filter Set"),
|
507
|
+
excitation=excitation,
|
508
|
+
emission=emission,
|
509
|
+
dichroic=dichroic,
|
510
|
+
)
|
511
|
+
|
512
|
+
def create_channels(self, config: Dict[str, Any]) -> Channels:
|
513
|
+
# Extract channel section
|
514
|
+
channel_config = config.get("channels", {})
|
515
|
+
if not channel_config:
|
516
|
+
raise ValueError("No channels configuration found in config")
|
517
|
+
channel_filters = []
|
518
|
+
channel_num = int(channel_config.get("num_of_channels"))
|
519
|
+
channel_names = channel_config.get("channel_names")
|
520
|
+
split_eff = channel_config.get("split_efficiency")
|
521
|
+
for i in range(channel_num):
|
522
|
+
channel_filters.append(
|
523
|
+
self.create_filter_set_from_config(
|
524
|
+
channel_config.get("filters").get(channel_names[i])
|
525
|
+
)
|
526
|
+
)
|
527
|
+
channels = Channels(
|
528
|
+
filtersets=channel_filters,
|
529
|
+
num_channels=channel_num,
|
530
|
+
splitting_efficiency=split_eff,
|
531
|
+
names=channel_names,
|
532
|
+
)
|
533
|
+
return channels
|
534
|
+
|
535
|
+
def create_quantum_efficiency_from_config(
|
536
|
+
self, qe_data: List[List[float]]
|
537
|
+
) -> QuantumEfficiency:
|
538
|
+
"""
|
539
|
+
Create a QuantumEfficiency instance from configuration data.
|
540
|
+
|
541
|
+
Args:
|
542
|
+
qe_data: List of [wavelength, efficiency] pairs
|
543
|
+
|
544
|
+
Returns:
|
545
|
+
QuantumEfficiency: Created quantum efficiency instance
|
546
|
+
"""
|
547
|
+
# Convert list of pairs to dictionary
|
548
|
+
wavelength_qe = {pair[0]: pair[1] for pair in qe_data}
|
549
|
+
return QuantumEfficiency(wavelength_qe=wavelength_qe)
|
550
|
+
|
551
|
+
def create_detector_from_config(
|
552
|
+
self, config: Dict[str, Any]
|
553
|
+
) -> Tuple[Detector, QuantumEfficiency]:
|
554
|
+
"""
|
555
|
+
Create a detector instance from a configuration dictionary.
|
556
|
+
|
557
|
+
Args:
|
558
|
+
config: Dictionary containing the full configuration (typically loaded from TOML)
|
559
|
+
|
560
|
+
Returns:
|
561
|
+
Tuple[Detector, QuantumEfficiency]: A tuple containing:
|
562
|
+
- Detector instance with the loaded configuration
|
563
|
+
- QuantumEfficiency instance for the detector
|
564
|
+
"""
|
565
|
+
# Extract camera section
|
566
|
+
camera_config = config.get("camera", {})
|
567
|
+
if not camera_config:
|
568
|
+
raise ValueError("No camera configuration found in config")
|
569
|
+
|
570
|
+
# Create quantum efficiency curve
|
571
|
+
qe_data = camera_config.get("quantum_efficiency", [])
|
572
|
+
quantum_efficiency = self.create_quantum_efficiency_from_config(qe_data)
|
573
|
+
|
574
|
+
pixel_size = self._find_pixel_size(
|
575
|
+
camera_config["magnification"], camera_config["pixel_detector_size"]
|
576
|
+
)
|
577
|
+
# Extract common parameters
|
578
|
+
common_params = {
|
579
|
+
"pixel_size": pixel_size,
|
580
|
+
"dark_current": float(camera_config["dark_current"]),
|
581
|
+
"readout_noise": float(camera_config["readout_noise"]),
|
582
|
+
"pixel_count": tuple([int(i) for i in camera_config["pixel_count"]]),
|
583
|
+
"bit_depth": int(camera_config.get("bit_depth", 16)),
|
584
|
+
"sensitivity": float(camera_config.get("sensitivity", 1.0)),
|
585
|
+
"pixel_detector_size": float(camera_config["pixel_detector_size"]),
|
586
|
+
"magnification": float(camera_config["magnification"]),
|
587
|
+
"base_adu": int(camera_config["base_adu"]),
|
588
|
+
"binning_size": int(camera_config["binning_size"]),
|
589
|
+
}
|
590
|
+
|
591
|
+
# Create appropriate detector based on type
|
592
|
+
camera_type = camera_config.get("type", "").upper()
|
593
|
+
|
594
|
+
if camera_type == "CMOS":
|
595
|
+
detector = CMOSDetector(**common_params)
|
596
|
+
elif camera_type == "EMCCD":
|
597
|
+
# Extract EMCCD-specific parameters
|
598
|
+
em_params = {
|
599
|
+
"em_gain": float(camera_config.get("em_gain", 300)),
|
600
|
+
"clock_induced_charge": float(
|
601
|
+
camera_config.get("clock_induced_charge", 0.002)
|
602
|
+
),
|
603
|
+
}
|
604
|
+
detector = EMCCDDetector(
|
605
|
+
**common_params,
|
606
|
+
em_gain=em_params["em_gain"],
|
607
|
+
clock_induced_charge=em_params["clock_induced_charge"],
|
608
|
+
)
|
609
|
+
else:
|
610
|
+
raise ValueError(f"Unsupported camera type: {camera_type}")
|
611
|
+
|
612
|
+
return detector, quantum_efficiency
|
613
|
+
|
614
|
+
def duration_time_validation_experiments(self, configEXP) -> bool:
|
615
|
+
if configEXP.exposure_time:
|
616
|
+
if len(configEXP.z_position) * (
|
617
|
+
configEXP.exposure_time + configEXP.interval_time
|
618
|
+
) > self.config["Global_Parameters"]["cycle_count"] * (
|
619
|
+
self.config["Global_Parameters"]["exposure_time"]
|
620
|
+
+ self.config["Global_Parameters"]["interval_time"]
|
621
|
+
):
|
622
|
+
print(
|
623
|
+
f"Z-series parameters overriding the set Global_parameters. cycle_count: {len(configEXP.z_position)}, exposure_time: {configEXP.exposure_time}, and interval_time: {configEXP.interval_time}."
|
624
|
+
)
|
625
|
+
self.config["Global_Parameters"]["cycle_count"] = len(
|
626
|
+
configEXP.z_position
|
627
|
+
)
|
628
|
+
self.config["Global_Parameters"]["exposure_time"] = (
|
629
|
+
configEXP.exposure_time
|
630
|
+
)
|
631
|
+
self.config["Global_Parameters"]["interval_time"] = (
|
632
|
+
configEXP.interval_time
|
633
|
+
)
|
634
|
+
|
635
|
+
return False
|
636
|
+
else:
|
637
|
+
return True
|
638
|
+
else:
|
639
|
+
return True
|
640
|
+
|
641
|
+
def setup_microscope(self) -> dict:
|
642
|
+
# config of experiment
|
643
|
+
|
644
|
+
configEXP, funcEXP = self.create_experiment_from_config(config=self.config)
|
645
|
+
self.duration_time_validation_experiments(configEXP)
|
646
|
+
# find the larger of the two duration times.
|
647
|
+
# base config
|
648
|
+
self.populate_dataclass_schema()
|
649
|
+
base_config = ConfigList(
|
650
|
+
CellParameters=self.cell_params,
|
651
|
+
MoleculeParameters=self.molecule_params,
|
652
|
+
GlobalParameters=self.global_params,
|
653
|
+
CondensateParameters=self.condensate_params,
|
654
|
+
OutputParameters=self.output_params,
|
655
|
+
)
|
656
|
+
|
657
|
+
# fluorophore config
|
658
|
+
fluorophores = self.create_fluorophores_from_config(self.config)
|
659
|
+
# psf config
|
660
|
+
psf, psf_config = self.create_psf_from_config(self.config)
|
661
|
+
# lasers config
|
662
|
+
lasers = self.create_lasers_from_config(self.config)
|
663
|
+
# channels config
|
664
|
+
channels = self.create_channels(self.config)
|
665
|
+
# detector config
|
666
|
+
detector, qe = self.create_detector_from_config(self.config)
|
667
|
+
|
668
|
+
# make cell
|
669
|
+
cell = make_cell(cell_params=base_config.CellParameters)
|
670
|
+
|
671
|
+
# make initial sample plane
|
672
|
+
sample_plane = make_sample(
|
673
|
+
global_params=base_config.GlobalParameters,
|
674
|
+
cell_params=base_config.CellParameters,
|
675
|
+
)
|
676
|
+
|
677
|
+
# make condensates_dict
|
678
|
+
condensates_dict = make_condensatedict(
|
679
|
+
condensate_params=base_config.CondensateParameters, cell=cell
|
680
|
+
)
|
681
|
+
|
682
|
+
# make sampling function
|
683
|
+
sampling_functions = make_samplingfunction(
|
684
|
+
condensate_params=base_config.CondensateParameters, cell=cell
|
685
|
+
)
|
686
|
+
|
687
|
+
# create initial positions
|
688
|
+
initial_molecule_positions = gen_initial_positions(
|
689
|
+
molecule_params=base_config.MoleculeParameters,
|
690
|
+
cell=cell,
|
691
|
+
condensate_params=base_config.CondensateParameters,
|
692
|
+
sampling_functions=sampling_functions,
|
693
|
+
)
|
694
|
+
|
695
|
+
# create the track generator
|
696
|
+
track_generators = create_track_generator(
|
697
|
+
global_params=base_config.GlobalParameters, cell=cell
|
698
|
+
)
|
699
|
+
|
700
|
+
# get all the tracks
|
701
|
+
tracks, points_per_time = get_tracks(
|
702
|
+
molecule_params=base_config.MoleculeParameters,
|
703
|
+
global_params=base_config.GlobalParameters,
|
704
|
+
initial_positions=initial_molecule_positions,
|
705
|
+
track_generator=track_generators,
|
706
|
+
)
|
707
|
+
|
708
|
+
# add tracks to sample
|
709
|
+
sample_plane = add_tracks_to_sample(
|
710
|
+
tracks=tracks, sample_plane=sample_plane, fluorophore=fluorophores
|
711
|
+
)
|
712
|
+
|
713
|
+
vm = VirtualMicroscope(
|
714
|
+
camera=(detector, qe),
|
715
|
+
sample_plane=sample_plane,
|
716
|
+
lasers=lasers,
|
717
|
+
channels=channels,
|
718
|
+
psf=psf,
|
719
|
+
config=base_config,
|
720
|
+
)
|
721
|
+
return_dict = {
|
722
|
+
"microscope": vm,
|
723
|
+
"base_config": base_config,
|
724
|
+
"psf": psf,
|
725
|
+
"psf_config": psf_config,
|
726
|
+
"channels": channels,
|
727
|
+
"lasers": lasers,
|
728
|
+
"sample_plane": sample_plane,
|
729
|
+
"tracks": tracks,
|
730
|
+
"points_per_time": points_per_time,
|
731
|
+
"condensate_dict": condensates_dict,
|
732
|
+
"cell": cell,
|
733
|
+
"experiment_config": configEXP,
|
734
|
+
"experiment_func": funcEXP,
|
735
|
+
}
|
736
|
+
return return_dict
|
737
|
+
|
738
|
+
|
739
|
+
def make_cell(cell_params) -> BaseCell:
|
740
|
+
# make cell
|
741
|
+
cell_origin = (cell_params.cell_space[0][0], cell_params.cell_space[1][0])
|
742
|
+
cell_dimensions = (
|
743
|
+
cell_params.cell_space[0][1] - cell_params.cell_space[0][0],
|
744
|
+
cell_params.cell_space[1][1] - cell_params.cell_space[1][0],
|
745
|
+
cell_params.cell_axial_radius * 2,
|
746
|
+
)
|
747
|
+
cell = RectangularCell(origin=cell_origin, dimensions=cell_dimensions)
|
748
|
+
|
749
|
+
return cell
|
750
|
+
|
751
|
+
|
752
|
+
def make_sample(global_params, cell_params) -> SamplePlane:
|
753
|
+
sample_space = SampleSpace(
|
754
|
+
x_max=global_params.sample_plane_dim[0],
|
755
|
+
y_max=global_params.sample_plane_dim[1],
|
756
|
+
z_max=cell_params.cell_axial_radius,
|
757
|
+
z_min=-cell_params.cell_axial_radius,
|
758
|
+
)
|
759
|
+
|
760
|
+
# total time
|
761
|
+
totaltime = int(
|
762
|
+
global_params.cycle_count
|
763
|
+
* (global_params.exposure_time + global_params.interval_time)
|
764
|
+
)
|
765
|
+
# initialize sample plane
|
766
|
+
sample_plane = SamplePlane(
|
767
|
+
sample_space=sample_space,
|
768
|
+
fov=(
|
769
|
+
(0, global_params.sample_plane_dim[0]),
|
770
|
+
(0, global_params.sample_plane_dim[1]),
|
771
|
+
(-cell_params.cell_axial_radius, cell_params.cell_axial_radius),
|
772
|
+
),
|
773
|
+
oversample_motion_time=global_params.oversample_motion_time,
|
774
|
+
t_end=totaltime,
|
775
|
+
)
|
776
|
+
return sample_plane
|
777
|
+
|
778
|
+
|
779
|
+
def make_condensatedict(condensate_params, cell) -> List[dict]:
|
780
|
+
condensates_dict = []
|
781
|
+
for i in range(len(condensate_params.initial_centers)):
|
782
|
+
condensates_dict.append(
|
783
|
+
create_condensate_dict(
|
784
|
+
initial_centers=condensate_params.initial_centers[i],
|
785
|
+
initial_scale=condensate_params.initial_scale[i],
|
786
|
+
diffusion_coefficient=condensate_params.diffusion_coefficient[i],
|
787
|
+
hurst_exponent=condensate_params.hurst_exponent[i],
|
788
|
+
cell=cell,
|
789
|
+
)
|
790
|
+
)
|
791
|
+
return condensates_dict
|
792
|
+
|
793
|
+
|
794
|
+
def make_samplingfunction(condensate_params, cell) -> List[Callable]:
|
795
|
+
sampling_functions = []
|
796
|
+
for i in range(len(condensate_params.initial_centers)):
|
797
|
+
sampling_functions.append(
|
798
|
+
tp(
|
799
|
+
num_subspace=len(condensate_params.initial_centers[i]),
|
800
|
+
subspace_centers=condensate_params.initial_centers[i],
|
801
|
+
subspace_radius=condensate_params.initial_scale[i],
|
802
|
+
density_dif=condensate_params.density_dif[i],
|
803
|
+
cell=cell,
|
804
|
+
)
|
805
|
+
)
|
806
|
+
return sampling_functions
|
807
|
+
|
808
|
+
|
809
|
+
def gen_initial_positions(molecule_params, cell, condensate_params, sampling_functions):
|
810
|
+
initials = []
|
811
|
+
for i in range(len(molecule_params.num_molecules)):
|
812
|
+
num_molecules = molecule_params.num_molecules[i]
|
813
|
+
initial_positions = gen_points(
|
814
|
+
pdf=sampling_functions[i],
|
815
|
+
total_points=num_molecules,
|
816
|
+
min_x=cell.origin[0],
|
817
|
+
max_x=cell.origin[0] + cell.dimensions[0],
|
818
|
+
min_y=cell.origin[1],
|
819
|
+
max_y=cell.origin[1] + cell.dimensions[1],
|
820
|
+
min_z=-cell.dimensions[2] / 2,
|
821
|
+
max_z=cell.dimensions[2] / 2,
|
822
|
+
density_dif=condensate_params.density_dif[i],
|
823
|
+
)
|
824
|
+
initials.append(initial_positions)
|
825
|
+
return initials
|
826
|
+
|
827
|
+
|
828
|
+
def create_track_generator(global_params, cell):
|
829
|
+
totaltime = int(
|
830
|
+
global_params.cycle_count
|
831
|
+
* (global_params.exposure_time + global_params.interval_time)
|
832
|
+
)
|
833
|
+
# make track generator
|
834
|
+
track_generator = Track_generator(
|
835
|
+
cell=cell,
|
836
|
+
cycle_count=totaltime / global_params.oversample_motion_time,
|
837
|
+
exposure_time=global_params.exposure_time,
|
838
|
+
interval_time=global_params.interval_time,
|
839
|
+
oversample_motion_time=global_params.oversample_motion_time,
|
840
|
+
)
|
841
|
+
return track_generator
|
842
|
+
|
843
|
+
|
844
|
+
def get_tracks(molecule_params, global_params, initial_positions, track_generator):
|
845
|
+
totaltime = int(
|
846
|
+
global_params.cycle_count
|
847
|
+
* (global_params.exposure_time + global_params.interval_time)
|
848
|
+
)
|
849
|
+
tracks_collection = []
|
850
|
+
points_per_time_collection = []
|
851
|
+
|
852
|
+
for i in range(len(initial_positions)):
|
853
|
+
if molecule_params.track_type[i] == "constant":
|
854
|
+
tracks, points_per_time = _generate_constant_tracks(
|
855
|
+
track_generator,
|
856
|
+
int(totaltime / global_params.oversample_motion_time),
|
857
|
+
initial_positions[i],
|
858
|
+
0,
|
859
|
+
)
|
860
|
+
elif molecule_params.allow_transition_probability[i]:
|
861
|
+
tracks, points_per_time = _generate_transition_tracks(
|
862
|
+
track_generator=track_generator,
|
863
|
+
track_lengths=int(totaltime / global_params.oversample_motion_time),
|
864
|
+
initial_positions=initial_positions[i],
|
865
|
+
starting_frames=0,
|
866
|
+
diffusion_parameters=molecule_params.diffusion_coefficient[i],
|
867
|
+
hurst_parameters=molecule_params.hurst_exponent[i],
|
868
|
+
diffusion_transition_matrix=change_prob_time(
|
869
|
+
molecule_params.diffusion_transition_matrix[i],
|
870
|
+
molecule_params.transition_matrix_time_step[i],
|
871
|
+
global_params.oversample_motion_time,
|
872
|
+
),
|
873
|
+
hurst_transition_matrix=change_prob_time(
|
874
|
+
molecule_params.hurst_transition_matrix[i],
|
875
|
+
molecule_params.transition_matrix_time_step[i],
|
876
|
+
global_params.oversample_motion_time,
|
877
|
+
),
|
878
|
+
diffusion_state_probability=molecule_params.state_probability_diffusion[
|
879
|
+
i
|
880
|
+
],
|
881
|
+
hurst_state_probability=molecule_params.state_probability_hurst[i],
|
882
|
+
)
|
883
|
+
else:
|
884
|
+
tracks, points_per_time = _generate_no_transition_tracks(
|
885
|
+
track_generator=track_generator,
|
886
|
+
track_lengths=int(totaltime / global_params.oversample_motion_time),
|
887
|
+
initial_positions=initial_positions[i],
|
888
|
+
starting_frames=0,
|
889
|
+
diffusion_parameters=molecule_params.diffusion_coefficient[i],
|
890
|
+
hurst_parameters=molecule_params.hurst_exponent[i],
|
891
|
+
)
|
892
|
+
|
893
|
+
tracks_collection.append(tracks)
|
894
|
+
points_per_time_collection.append(points_per_time)
|
895
|
+
|
896
|
+
return tracks_collection, points_per_time_collection
|
897
|
+
|
898
|
+
|
899
|
+
def add_tracks_to_sample(tracks, sample_plane, fluorophore, ID_counter=0):
|
900
|
+
counter = ID_counter
|
901
|
+
for track_type in range(len(tracks)):
|
902
|
+
for j in tracks[track_type].values():
|
903
|
+
sample_plane.add_object(
|
904
|
+
object_id=str(counter),
|
905
|
+
position=j["xy"][0],
|
906
|
+
fluorophore=fluorophore[track_type],
|
907
|
+
trajectory=_convert_tracks_to_trajectory(j),
|
908
|
+
)
|
909
|
+
counter += 1
|
910
|
+
return sample_plane
|