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.
Files changed (55) hide show
  1. AMS_BP/__init__.py +13 -0
  2. AMS_BP/cells/__init__.py +5 -0
  3. AMS_BP/cells/base_cell.py +55 -0
  4. AMS_BP/cells/rectangular_cell.py +82 -0
  5. AMS_BP/cells/rod_cell.py +98 -0
  6. AMS_BP/cells/spherical_cell.py +74 -0
  7. AMS_BP/configio/__init__.py +0 -0
  8. AMS_BP/configio/configmodels.py +93 -0
  9. AMS_BP/configio/convertconfig.py +910 -0
  10. AMS_BP/configio/experiments.py +121 -0
  11. AMS_BP/configio/saving.py +32 -0
  12. AMS_BP/metadata/__init__.py +0 -0
  13. AMS_BP/metadata/metadata.py +87 -0
  14. AMS_BP/motion/__init__.py +4 -0
  15. AMS_BP/motion/condensate_movement.py +356 -0
  16. AMS_BP/motion/movement/__init__.py +10 -0
  17. AMS_BP/motion/movement/boundary_conditions.py +75 -0
  18. AMS_BP/motion/movement/fbm_BP.py +244 -0
  19. AMS_BP/motion/track_gen.py +541 -0
  20. AMS_BP/optics/__init__.py +0 -0
  21. AMS_BP/optics/camera/__init__.py +4 -0
  22. AMS_BP/optics/camera/detectors.py +320 -0
  23. AMS_BP/optics/camera/quantum_eff.py +66 -0
  24. AMS_BP/optics/filters/__init__.py +17 -0
  25. AMS_BP/optics/filters/channels/__init__.py +0 -0
  26. AMS_BP/optics/filters/channels/channelschema.py +27 -0
  27. AMS_BP/optics/filters/filters.py +184 -0
  28. AMS_BP/optics/lasers/__init__.py +28 -0
  29. AMS_BP/optics/lasers/laser_profiles.py +691 -0
  30. AMS_BP/optics/psf/__init__.py +7 -0
  31. AMS_BP/optics/psf/psf_engine.py +215 -0
  32. AMS_BP/photophysics/__init__.py +0 -0
  33. AMS_BP/photophysics/photon_physics.py +181 -0
  34. AMS_BP/photophysics/state_kinetics.py +146 -0
  35. AMS_BP/probabilityfuncs/__init__.py +0 -0
  36. AMS_BP/probabilityfuncs/markov_chain.py +143 -0
  37. AMS_BP/probabilityfuncs/probability_functions.py +350 -0
  38. AMS_BP/run_cell_simulation.py +217 -0
  39. AMS_BP/sample/__init__.py +0 -0
  40. AMS_BP/sample/flurophores/__init__.py +16 -0
  41. AMS_BP/sample/flurophores/flurophore_schema.py +290 -0
  42. AMS_BP/sample/sim_sampleplane.py +334 -0
  43. AMS_BP/sim_config.toml +418 -0
  44. AMS_BP/sim_microscopy.py +453 -0
  45. AMS_BP/utils/__init__.py +0 -0
  46. AMS_BP/utils/constants.py +11 -0
  47. AMS_BP/utils/decorators.py +227 -0
  48. AMS_BP/utils/errors.py +37 -0
  49. AMS_BP/utils/maskMaker.py +12 -0
  50. AMS_BP/utils/util_functions.py +319 -0
  51. ams_bp-0.0.2.dist-info/METADATA +173 -0
  52. ams_bp-0.0.2.dist-info/RECORD +55 -0
  53. ams_bp-0.0.2.dist-info/WHEEL +4 -0
  54. ams_bp-0.0.2.dist-info/entry_points.txt +2 -0
  55. 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