AMS-BP 0.0.231__py3-none-any.whl → 0.2.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.
- AMS_BP/__init__.py +1 -1
- AMS_BP/cells/__init__.py +30 -4
- AMS_BP/cells/budding_yeast_cell.py +274 -0
- AMS_BP/cells/cell_factory.py +148 -0
- AMS_BP/configio/configmodels.py +9 -6
- AMS_BP/configio/convertconfig.py +37 -29
- AMS_BP/groundtruth_generators/__init__.py +3 -0
- AMS_BP/groundtruth_generators/nuclearporecomplexes.py +68 -0
- AMS_BP/motion/condensate_movement.py +28 -29
- AMS_BP/motion/movement/__init__.py +1 -6
- AMS_BP/motion/movement/boundary_conditions.py +12 -1
- AMS_BP/motion/track_gen.py +35 -84
- AMS_BP/optics/lasers/laser_profiles.py +27 -185
- AMS_BP/optics/lasers/scanning_patterns.py +102 -0
- AMS_BP/optics/psf/psf_engine.py +25 -8
- AMS_BP/photophysics/photon_physics.py +4 -4
- AMS_BP/photophysics/state_kinetics.py +37 -2
- AMS_BP/probabilityfuncs/probability_functions.py +33 -100
- AMS_BP/sample/sim_sampleplane.py +55 -24
- AMS_BP/sim_config.toml +13 -8
- AMS_BP/sim_microscopy.py +40 -9
- AMS_BP/utils/util_functions.py +9 -0
- {ams_bp-0.0.231.dist-info → ams_bp-0.2.0.dist-info}/METADATA +25 -4
- {ams_bp-0.0.231.dist-info → ams_bp-0.2.0.dist-info}/RECORD +27 -27
- AMS_BP/cells/base_cell.py +0 -55
- AMS_BP/cells/rectangular_cell.py +0 -82
- AMS_BP/cells/rod_cell.py +0 -98
- AMS_BP/cells/spherical_cell.py +0 -74
- AMS_BP/motion/movement/fbm_BP.py +0 -244
- {ams_bp-0.0.231.dist-info → ams_bp-0.2.0.dist-info}/WHEEL +0 -0
- {ams_bp-0.0.231.dist-info → ams_bp-0.2.0.dist-info}/entry_points.txt +0 -0
- {ams_bp-0.0.231.dist-info → ams_bp-0.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -23,17 +23,21 @@ class LaserParameters:
|
|
23
23
|
|
24
24
|
wavelength: float # Wavelength in nanometers
|
25
25
|
power: Union[float, Callable[[float], float]] # Power in watts
|
26
|
-
beam_width: float # 1/e² beam width at waist in microns
|
26
|
+
beam_width: Optional[float] = None # 1/e² beam width at waist in microns
|
27
27
|
numerical_aperture: Optional[float] = None # NA of focusing lens
|
28
|
-
position: Union[
|
28
|
+
position: Union[
|
29
|
+
Tuple[float, float, float], Callable[[float], Tuple[float, float, float]]
|
30
|
+
] = (
|
29
31
|
0.0,
|
30
32
|
0.0,
|
31
33
|
0.0,
|
32
34
|
)
|
33
|
-
refractive_index: float = 1.0 # Refractive index of medium
|
35
|
+
refractive_index: Optional[float] = 1.0 # Refractive index of medium
|
34
36
|
|
35
37
|
def __post_init__(self):
|
36
38
|
"""Validate parameters after initialization."""
|
39
|
+
if not self.beam_width:
|
40
|
+
self.beam_width = self.diffraction_limited_width
|
37
41
|
self._validate_parameters()
|
38
42
|
self._compute_derived_parameters()
|
39
43
|
self.max_power = self.power
|
@@ -96,7 +100,10 @@ class LaserParameters:
|
|
96
100
|
Power in watts
|
97
101
|
"""
|
98
102
|
if callable(self.power):
|
99
|
-
|
103
|
+
power = self.power(t)
|
104
|
+
if power < 0:
|
105
|
+
raise ValueError("Laser Power Cannot be Negative")
|
106
|
+
return power
|
100
107
|
return self.power
|
101
108
|
|
102
109
|
def get_position(self, t: float) -> Tuple[float, float, float]:
|
@@ -228,7 +235,7 @@ class LaserProfile(ABC):
|
|
228
235
|
class GaussianBeam(LaserProfile):
|
229
236
|
"""3D Gaussian laser beam profile with time dependence."""
|
230
237
|
|
231
|
-
def
|
238
|
+
def calculate_intensity_(
|
232
239
|
self,
|
233
240
|
x: np.ndarray | float,
|
234
241
|
y: np.ndarray | float,
|
@@ -267,7 +274,7 @@ class GaussianBeam(LaserProfile):
|
|
267
274
|
# * np.cos(phase_terms)
|
268
275
|
)
|
269
276
|
|
270
|
-
def
|
277
|
+
def calculate_intensity(
|
271
278
|
self,
|
272
279
|
x: np.ndarray | float,
|
273
280
|
y: np.ndarray | float,
|
@@ -305,7 +312,7 @@ class GaussianBeam(LaserProfile):
|
|
305
312
|
w_z = self.get_beam_width(z_shifted)
|
306
313
|
|
307
314
|
# Calculate peak intensity (z-dependent)
|
308
|
-
I0 = 2 * power / (np.pi * (self.params.beam_width
|
315
|
+
I0 = 2 * power / (np.pi * (self.params.beam_width) ** 2)
|
309
316
|
I0_z = I0 * (self.params.beam_width / w_z) ** 2
|
310
317
|
|
311
318
|
# Calculate phase terms if needed
|
@@ -368,15 +375,16 @@ class WidefieldBeam(LaserProfile):
|
|
368
375
|
Returns:
|
369
376
|
Intensity scaling factor between 0 and 1
|
370
377
|
"""
|
371
|
-
# Use error function for smooth transition at DoF boundaries
|
372
|
-
# Scale factor determines how sharp the transition is
|
373
|
-
scale_factor = 2.0 # Adjust this to change transition sharpness
|
374
|
-
|
375
|
-
# Normalize z by DoF and create smooth falloff
|
376
|
-
normalized_z = scale_factor * (np.abs(z)
|
377
|
-
|
378
|
-
# Use sigmoid function for smooth transition
|
379
|
-
return 1 / (1 + np.exp(normalized_z))
|
378
|
+
# # Use error function for smooth transition at DoF boundaries
|
379
|
+
# # Scale factor determines how sharp the transition is
|
380
|
+
# scale_factor = 2.0 # Adjust this to change transition sharpness
|
381
|
+
#
|
382
|
+
# # Normalize z by DoF and create smooth falloff
|
383
|
+
# normalized_z = scale_factor * (np.abs(z)) / self.dof
|
384
|
+
#
|
385
|
+
# # Use sigmoid function for smooth transition
|
386
|
+
# return 1 / (1 + np.exp(normalized_z))
|
387
|
+
return 1.0
|
380
388
|
|
381
389
|
def calculate_intensity(
|
382
390
|
self,
|
@@ -413,7 +421,7 @@ class WidefieldBeam(LaserProfile):
|
|
413
421
|
base_intensity = power / (np.pi * self.max_radius**2)
|
414
422
|
|
415
423
|
# Apply radial intensity profile with smooth falloff at edges
|
416
|
-
edge_width = self.max_radius * 0.
|
424
|
+
edge_width = self.max_radius * 0.00001
|
417
425
|
radial_profile = 0.5 * (1 - np.tanh((r - self.max_radius) / edge_width))
|
418
426
|
# Apply DoF-based axial intensity profile
|
419
427
|
axial_profile = self._calculate_dof_profile(z_shifted)
|
@@ -422,32 +430,6 @@ class WidefieldBeam(LaserProfile):
|
|
422
430
|
return base_intensity * radial_profile * axial_profile
|
423
431
|
|
424
432
|
|
425
|
-
# Example usage
|
426
|
-
if __name__ == "__main__":
|
427
|
-
# Create parameters for a typical microscope objective
|
428
|
-
params = LaserParameters(
|
429
|
-
wavelength=488, # 488 nm
|
430
|
-
power=0.001, # 1 mW
|
431
|
-
beam_width=0.25, # 250 nm
|
432
|
-
numerical_aperture=1.4,
|
433
|
-
refractive_index=1.518, # Oil immersion
|
434
|
-
)
|
435
|
-
|
436
|
-
# Create beam object
|
437
|
-
beam = GaussianBeam(params)
|
438
|
-
|
439
|
-
# Get intensity map
|
440
|
-
result = beam.get_intensity_map(
|
441
|
-
volume_size=(5, 5, 10), # 5x5x10 microns
|
442
|
-
voxel_size=0.1, # 100 nm voxels
|
443
|
-
t=0, # t=0 seconds
|
444
|
-
)
|
445
|
-
|
446
|
-
# print(f"Beam waist: {params.beam_width:.3f} µm")
|
447
|
-
# print(f"Rayleigh range: {params.rayleigh_range:.3f} µm")
|
448
|
-
# print(f"Diffraction limit: {params.diffraction_limited_width:.3f} µm")
|
449
|
-
|
450
|
-
|
451
433
|
class HiLoBeam(LaserProfile):
|
452
434
|
"""
|
453
435
|
Highly Inclined Laminated Optical (HiLo) illumination profile.
|
@@ -522,7 +504,7 @@ class HiLoBeam(LaserProfile):
|
|
522
504
|
z_shifted = z - pos[2]
|
523
505
|
|
524
506
|
# Calculate radial distance from optical axis
|
525
|
-
|
507
|
+
r_squared = x_shifted**2 + y_shifted**2
|
526
508
|
|
527
509
|
# Base beam parameters
|
528
510
|
w0 = self.params.beam_width # Beam waist
|
@@ -542,150 +524,10 @@ class HiLoBeam(LaserProfile):
|
|
542
524
|
intensity = (
|
543
525
|
I0
|
544
526
|
* (w0 / w_z) ** 2 # Beam width scaling
|
545
|
-
* np.exp(-2 *
|
527
|
+
* np.exp(-2 * r_squared / w_z**2) # Gaussian radial profile
|
546
528
|
)
|
547
529
|
|
548
530
|
# Lamination effect: attenuate out-of-focus regions
|
549
531
|
lamination_factor = np.exp(-np.abs(z_shifted) / (2 * self.axial_resolution))
|
550
532
|
|
551
533
|
return intensity * lamination_factor
|
552
|
-
|
553
|
-
|
554
|
-
class ConfocalBeam(LaserProfile):
|
555
|
-
"""
|
556
|
-
Confocal microscopy beam profile with point scanning and pinhole characteristics.
|
557
|
-
|
558
|
-
Implements key optical principles of confocal microscopy:
|
559
|
-
- Point scanning illumination
|
560
|
-
- Pinhole-based rejection of out-of-focus light
|
561
|
-
- Depth-resolved imaging capabilities
|
562
|
-
"""
|
563
|
-
|
564
|
-
def __init__(
|
565
|
-
self,
|
566
|
-
params: LaserParameters,
|
567
|
-
pinhole_diameter: float, # Pinhole diameter in microns
|
568
|
-
scanning_mode: str = "point", # 'point' or 'line'
|
569
|
-
line_orientation: str = "horizontal", # 'horizontal' or 'vertical'
|
570
|
-
):
|
571
|
-
"""
|
572
|
-
Initialize Confocal beam profile.
|
573
|
-
|
574
|
-
Args:
|
575
|
-
params: LaserParameters for the beam
|
576
|
-
pinhole_diameter: Diameter of the detection pinhole in microns
|
577
|
-
scanning_mode: Scanning method ('point' or 'line')
|
578
|
-
line_orientation: Orientation for line scanning
|
579
|
-
"""
|
580
|
-
super().__init__(params)
|
581
|
-
|
582
|
-
# Validate numerical aperture
|
583
|
-
if params.numerical_aperture is None:
|
584
|
-
raise ValueError(
|
585
|
-
"Numerical aperture must be specified for confocal microscopy"
|
586
|
-
)
|
587
|
-
|
588
|
-
# Pinhole and optical characteristics
|
589
|
-
self.pinhole_diameter = pinhole_diameter
|
590
|
-
self.scanning_mode = scanning_mode
|
591
|
-
self.line_orientation = line_orientation
|
592
|
-
|
593
|
-
# Calculate optical parameters
|
594
|
-
wavelength_microns = params.wavelength / 1000.0
|
595
|
-
na = params.numerical_aperture
|
596
|
-
|
597
|
-
# Theoretical resolution calculations
|
598
|
-
self.lateral_resolution = 0.61 * wavelength_microns / na
|
599
|
-
self.axial_resolution = 0.5 * wavelength_microns / (na**2)
|
600
|
-
|
601
|
-
# Pinhole transmission calculation
|
602
|
-
# Airy disk radius calculation
|
603
|
-
self.airy_radius = 1.22 * wavelength_microns / (2 * na)
|
604
|
-
|
605
|
-
# Transmission through pinhole
|
606
|
-
def pinhole_transmission(z):
|
607
|
-
"""
|
608
|
-
Calculate pinhole transmission as a function of z-position.
|
609
|
-
Uses an error function to model smooth transition.
|
610
|
-
"""
|
611
|
-
# Normalized z-position relative to focal plane
|
612
|
-
z_norm = z / self.axial_resolution
|
613
|
-
|
614
|
-
# Smooth transition function
|
615
|
-
return 0.5 * (1 + np.tanh(-z_norm))
|
616
|
-
|
617
|
-
self.pinhole_transmission = pinhole_transmission
|
618
|
-
|
619
|
-
# print("Confocal Microscopy Configuration:")
|
620
|
-
# print(f" Scanning Mode: {scanning_mode}")
|
621
|
-
# print(f" Pinhole Diameter: {pinhole_diameter:.2f} µm")
|
622
|
-
# print(f" Lateral Resolution: {self.lateral_resolution:.3f} µm")
|
623
|
-
# print(f" Axial Resolution: {self.axial_resolution:.3f} µm")
|
624
|
-
# print(f" Airy Disk Radius: {self.airy_radius:.3f} µm")
|
625
|
-
|
626
|
-
def calculate_intensity(
|
627
|
-
self,
|
628
|
-
x: np.ndarray | float,
|
629
|
-
y: np.ndarray | float,
|
630
|
-
z: np.ndarray | float,
|
631
|
-
t: float,
|
632
|
-
) -> np.ndarray:
|
633
|
-
"""
|
634
|
-
Calculate the confocal illumination intensity distribution.
|
635
|
-
|
636
|
-
Args:
|
637
|
-
x: X coordinates in microns (3D array)
|
638
|
-
y: Y coordinates in microns (3D array)
|
639
|
-
z: Z coordinates in microns (3D array)
|
640
|
-
t: Time in seconds
|
641
|
-
|
642
|
-
Returns:
|
643
|
-
3D array of intensities in W/µm²
|
644
|
-
"""
|
645
|
-
# Get time-dependent parameters
|
646
|
-
power = self.params.get_power(t)
|
647
|
-
pos = self.params.get_position(t)
|
648
|
-
|
649
|
-
# Shift coordinates based on current beam position
|
650
|
-
x_shifted = x - pos[0]
|
651
|
-
y_shifted = y - pos[1]
|
652
|
-
z_shifted = z - pos[2]
|
653
|
-
|
654
|
-
# Base beam parameters
|
655
|
-
w0 = self.params.beam_width # Beam waist
|
656
|
-
zR = self.params.rayleigh_range # Rayleigh range
|
657
|
-
|
658
|
-
# Calculate beam width at z
|
659
|
-
w_z = w0 * np.sqrt(1 + (z_shifted / zR) ** 2)
|
660
|
-
|
661
|
-
# Peak intensity calculation
|
662
|
-
I0 = 2 * power / (np.pi * w0**2)
|
663
|
-
|
664
|
-
# Scanning mode intensity modification
|
665
|
-
if self.scanning_mode == "point":
|
666
|
-
# Point scanning: standard Gaussian beam
|
667
|
-
radial_intensity = (
|
668
|
-
I0
|
669
|
-
* (w0 / w_z) ** 2
|
670
|
-
* np.exp(-2 * (x_shifted**2 + y_shifted**2) / w_z**2)
|
671
|
-
)
|
672
|
-
elif self.scanning_mode == "line":
|
673
|
-
# Line scanning: different intensity distribution
|
674
|
-
if self.line_orientation == "horizontal":
|
675
|
-
line_intensity = (
|
676
|
-
I0 * (w0 / w_z) ** 2 * np.exp(-2 * y_shifted**2 / w_z**2)
|
677
|
-
)
|
678
|
-
radial_intensity = line_intensity
|
679
|
-
else: # vertical line scanning
|
680
|
-
line_intensity = (
|
681
|
-
I0 * (w0 / w_z) ** 2 * np.exp(-2 * x_shifted**2 / w_z**2)
|
682
|
-
)
|
683
|
-
radial_intensity = line_intensity
|
684
|
-
else:
|
685
|
-
raise ValueError(f"Unknown scanning mode: {self.scanning_mode}")
|
686
|
-
|
687
|
-
# Pinhole transmission effect
|
688
|
-
pinhole_effect = self.pinhole_transmission(z_shifted)
|
689
|
-
|
690
|
-
# Final intensity calculation
|
691
|
-
return radial_intensity * pinhole_effect
|
@@ -0,0 +1,102 @@
|
|
1
|
+
import math
|
2
|
+
from functools import partial
|
3
|
+
from typing import Callable, List, Tuple
|
4
|
+
|
5
|
+
import numpy as np
|
6
|
+
|
7
|
+
"""Currently unused module"""
|
8
|
+
|
9
|
+
|
10
|
+
def plane_point_scan(
|
11
|
+
x_lims: List[float],
|
12
|
+
y_lims: List[float],
|
13
|
+
step_xy: float,
|
14
|
+
) -> np.ndarray:
|
15
|
+
"""
|
16
|
+
Generate a point scanning pattern for a confocal microscope plane scan.
|
17
|
+
|
18
|
+
Args:
|
19
|
+
x_lims (List[float]): [min_x, max_x] scanning limits in x direction
|
20
|
+
y_lims (List[float]): [min_y, max_y] scanning limits in y direction
|
21
|
+
step_xy (float): Step size between points in both x and y directions
|
22
|
+
|
23
|
+
Returns:
|
24
|
+
np.ndarray: Array of shape (n_points, 2) containing [x, y] coordinates for the scan
|
25
|
+
"""
|
26
|
+
# Calculate number of points in each dimension
|
27
|
+
nx = math.ceil((x_lims[1] - x_lims[0]) / step_xy) + 1
|
28
|
+
ny = math.ceil((y_lims[1] - y_lims[0]) / step_xy) + 1
|
29
|
+
|
30
|
+
# Generate coordinate arrays
|
31
|
+
x = np.linspace(x_lims[0], x_lims[1], nx)
|
32
|
+
y = np.linspace(y_lims[0], y_lims[1], ny)
|
33
|
+
|
34
|
+
# Create meshgrid for all coordinates
|
35
|
+
xx, yy = np.meshgrid(x, y)
|
36
|
+
|
37
|
+
# Convert to scan pattern array
|
38
|
+
# For even rows, reverse x direction for serpentine scan
|
39
|
+
scan_points = []
|
40
|
+
for i in range(ny):
|
41
|
+
row_x = xx[i]
|
42
|
+
row_y = yy[i]
|
43
|
+
|
44
|
+
if i % 2 == 1: # Reverse even rows for serpentine pattern
|
45
|
+
row_x = row_x[::-1]
|
46
|
+
|
47
|
+
points = np.column_stack((row_x, row_y))
|
48
|
+
scan_points.append(points)
|
49
|
+
|
50
|
+
# Combine all points into final array
|
51
|
+
scan_pattern = np.vstack(scan_points)
|
52
|
+
|
53
|
+
return scan_pattern
|
54
|
+
|
55
|
+
|
56
|
+
def confocal_pointscan_time_z(
|
57
|
+
x_lims: List[float],
|
58
|
+
y_lims: List[float],
|
59
|
+
step_xy: float, # can be defined as the beam width at the focus plane
|
60
|
+
frame_exposure_time: float, # s
|
61
|
+
) -> Tuple[Callable[[float, float], Tuple[float, float, float]], float]:
|
62
|
+
scan_pattern = plane_point_scan(x_lims=x_lims, y_lims=y_lims, step_xy=step_xy)
|
63
|
+
scan_pattern_len = len(scan_pattern)
|
64
|
+
|
65
|
+
dwell_time = frame_exposure_time / scan_pattern_len
|
66
|
+
|
67
|
+
def return_laser_position(
|
68
|
+
z_position: float, time: float
|
69
|
+
) -> Tuple[float, float, float]:
|
70
|
+
index_frame = time % frame_exposure_time
|
71
|
+
ind = int(index_frame / dwell_time)
|
72
|
+
# print(index_frame, ind)
|
73
|
+
return (*scan_pattern[ind], z_position)
|
74
|
+
|
75
|
+
return return_laser_position, dwell_time
|
76
|
+
|
77
|
+
|
78
|
+
def confocal_pointscan_time_z0(
|
79
|
+
x_lims: List[float],
|
80
|
+
y_lims: List[float],
|
81
|
+
step_xy: float, # can be defined as the beam width at the focus plane
|
82
|
+
frame_exposure_time: float, # s
|
83
|
+
z_val: float, # um
|
84
|
+
) -> Tuple[Callable[[float], Tuple[float, float, float]], float]:
|
85
|
+
"""
|
86
|
+
Create a generator for a point scanning pattern for a confocal microscope plane scan which takes in a time and returns the postion of the laser.
|
87
|
+
|
88
|
+
Args:
|
89
|
+
x_lims (List[float]): [min_x, max_x] scanning limits in x direction
|
90
|
+
y_lims (List[float]): [min_y, max_y] scanning limits in y direction
|
91
|
+
step_xy (float): Step size between points in both x and y directions
|
92
|
+
frame_exposure_time (float): exposure time of the frame
|
93
|
+
z_val (float): z value of the sample plane
|
94
|
+
|
95
|
+
Returns:
|
96
|
+
Callable[time]: (x,y,z) position of the laser
|
97
|
+
dwell_time (float): the dwell time per position
|
98
|
+
"""
|
99
|
+
func, dwell_time = confocal_pointscan_time_z(
|
100
|
+
x_lims, y_lims, step_xy, frame_exposure_time
|
101
|
+
)
|
102
|
+
return partial(func, z_val), dwell_time
|
AMS_BP/optics/psf/psf_engine.py
CHANGED
@@ -85,15 +85,19 @@ class PSFEngine:
|
|
85
85
|
self._grid_xy = _generate_grid(self._psf_size, self.params.pixel_size)
|
86
86
|
|
87
87
|
# Pre-calculate normalized sigma values
|
88
|
-
self._norm_sigma_xy = self._sigma_xy / 2.
|
89
|
-
self._norm_sigma_z = self._sigma_z / 2.
|
88
|
+
self._norm_sigma_xy = self._sigma_xy / 2.0
|
89
|
+
self._norm_sigma_z = self._sigma_z / 2.0
|
90
90
|
|
91
91
|
# Generate pinhole mask if specified
|
92
92
|
if self.params.pinhole_radius is not None:
|
93
93
|
if self.params.pinhole_radius < AIRYFACTOR * self._sigma_xy:
|
94
|
-
|
94
|
+
RuntimeWarning(
|
95
95
|
f"Pinhole size ({self.params.pinhole_radius} um) is smaller than {AIRYFACTOR} times the Airy lobe. This will diffract the emission light in the pinhole; an ideal pinhole size for this setup is {self._sigma_xy} um."
|
96
96
|
)
|
97
|
+
#
|
98
|
+
# raise ValueError(
|
99
|
+
# f"Pinhole size ({self.params.pinhole_radius} um) is smaller than {AIRYFACTOR} times the Airy lobe. This will diffract the emission light in the pinhole; an ideal pinhole size for this setup is {self._sigma_xy} um."
|
100
|
+
# )
|
97
101
|
self._pinhole_mask = self._generate_pinhole_mask()
|
98
102
|
else:
|
99
103
|
self._pinhole_mask = None
|
@@ -116,7 +120,9 @@ class PSFEngine:
|
|
116
120
|
return (r <= self.params.pinhole_radius).astype(np.float64)
|
117
121
|
|
118
122
|
@lru_cache(maxsize=128)
|
119
|
-
def psf_z(
|
123
|
+
def psf_z(
|
124
|
+
self, x_val: float, y_val: float, z_val: float, norm_scale: bool = True
|
125
|
+
) -> NDArray[np.float64]:
|
120
126
|
"""Calculate the PSF at the detector for a point source at z_val.
|
121
127
|
|
122
128
|
This represents how light from a point source at position z_val
|
@@ -124,17 +130,28 @@ class PSFEngine:
|
|
124
130
|
detector. If a pinhole is present, it spatially filters this pattern.
|
125
131
|
|
126
132
|
Args:
|
133
|
+
x_val: x-position of the point source in micrometers
|
134
|
+
y_val: y-position of the point source in micrometers
|
127
135
|
z_val: Z-position of the point source in micrometers
|
128
136
|
|
129
137
|
Returns:
|
130
138
|
2D array containing the light intensity pattern at the detector
|
131
139
|
"""
|
132
140
|
x, y = self._grid_xy
|
141
|
+
sigma_xy_z_squared = (self._norm_sigma_xy**2) * (
|
142
|
+
1 + (z_val / self._norm_sigma_z) ** 2
|
143
|
+
)
|
133
144
|
|
134
145
|
# Calculate how light from the point source diffracts through collection optics
|
135
|
-
r_squared = (x
|
136
|
-
|
137
|
-
|
146
|
+
r_squared = (x - x_val % self.params.pixel_size) ** 2 + (
|
147
|
+
y - y_val % self.params.pixel_size
|
148
|
+
) ** 2
|
149
|
+
psf_at_detector = np.exp(-0.5 * (r_squared / sigma_xy_z_squared))
|
150
|
+
|
151
|
+
if norm_scale:
|
152
|
+
psf_at_detector = self.normalize_psf(
|
153
|
+
psf_at_detector, mode="sum"
|
154
|
+
) * self.psf_z_xy0(z_val)
|
138
155
|
|
139
156
|
if self._pinhole_mask is not None:
|
140
157
|
# Apply pinhole's spatial filtering
|
@@ -252,7 +269,7 @@ def calculate_psf_size(
|
|
252
269
|
Tuple of dimensions (z,y,x) or (y,x) for the PSF calculation
|
253
270
|
"""
|
254
271
|
# Calculate radius to capture important features (2x Airy radius)
|
255
|
-
r_psf =
|
272
|
+
r_psf = 3 * sigma_xy
|
256
273
|
|
257
274
|
# Convert to pixels and ensure odd number
|
258
275
|
pixels_xy = int(np.ceil(r_psf / pixel_size))
|
@@ -168,11 +168,11 @@ class incident_photons:
|
|
168
168
|
photons_n = self.transmission_photon_rate.values[i] * dt
|
169
169
|
photons += photons_n
|
170
170
|
psf_gen = (
|
171
|
-
self.generator[i].
|
172
|
-
|
173
|
-
|
171
|
+
self.generator[i].psf_z(
|
172
|
+
x_val=self.position[0],
|
173
|
+
y_val=self.position[1],
|
174
|
+
z_val=self.position[2],
|
174
175
|
)
|
175
|
-
* self.generator[i].psf_z_xy0(z_val=self.position[2])
|
176
176
|
* photons_n
|
177
177
|
)
|
178
178
|
|
@@ -27,6 +27,7 @@ class StateTransitionCalculator:
|
|
27
27
|
self.current_global_time = current_global_time # ms (oversample motion time)
|
28
28
|
self.laser_intensity_generator = laser_intensity_generator
|
29
29
|
self.fluorescent_state_history = {} # {fluorescent.state.name : [delta time (seconds), laser_intensites], ...}
|
30
|
+
self.current_global_time_s = self.current_global_time * 1e-3
|
30
31
|
|
31
32
|
def __call__(
|
32
33
|
self,
|
@@ -44,13 +45,33 @@ class StateTransitionCalculator:
|
|
44
45
|
self.fluorescent_state_history[i.name] = [0, laser_intensities]
|
45
46
|
return laser_intensities
|
46
47
|
|
48
|
+
def _get_intensities(self, time_pos: int, time_laser: float) -> dict:
|
49
|
+
laser_intensities = self.laser_intensity_generator(
|
50
|
+
florPos=self.flurophoreobj.position_history[time_pos],
|
51
|
+
time=time_laser,
|
52
|
+
)
|
53
|
+
return laser_intensities
|
54
|
+
|
47
55
|
def MCMC(self) -> Tuple[State, ErnoMsg]:
|
48
56
|
time = 0
|
49
57
|
transitions = self.flurophoreobj.state_history[self.current_global_time][2]
|
58
|
+
if not transitions:
|
59
|
+
self.fluorescent_state_history[
|
60
|
+
self.flurophoreobj.fluorophore.states[
|
61
|
+
self.flurophoreobj.state_history[self.current_global_time][0].name
|
62
|
+
]
|
63
|
+
][0] += self.time_duration
|
64
|
+
return self.flurophoreobj.fluorophore.states[
|
65
|
+
self.flurophoreobj.state_history[self.current_global_time][0].name
|
66
|
+
], ErnoMsg(success=True)
|
50
67
|
final_state_name = transitions[0].from_state
|
51
|
-
laser_intensities = self._initialize_state_hist(
|
52
|
-
|
68
|
+
laser_intensities = self._initialize_state_hist(
|
69
|
+
self.current_global_time, time + self.current_global_time_s
|
70
|
+
)
|
53
71
|
while time < self.time_duration:
|
72
|
+
laser_intensities = self._get_intensities(
|
73
|
+
self.current_global_time, self.current_global_time_s + time
|
74
|
+
)
|
54
75
|
stateTransitionMatrixR = [
|
55
76
|
sum(
|
56
77
|
state_transitions.rate()(laser["wavelength"], laser["intensity"])
|
@@ -59,8 +80,22 @@ class StateTransitionCalculator:
|
|
59
80
|
for state_transitions in transitions
|
60
81
|
] # 1/s
|
61
82
|
if not stateTransitionMatrixR:
|
83
|
+
if (
|
84
|
+
self.flurophoreobj.fluorophore.states[final_state_name].state_type
|
85
|
+
== StateType.FLUORESCENT
|
86
|
+
):
|
87
|
+
self.fluorescent_state_history[
|
88
|
+
self.flurophoreobj.fluorophore.states[final_state_name].name
|
89
|
+
][0] += self.time_duration
|
62
90
|
break
|
63
91
|
if sum(stateTransitionMatrixR) == 0:
|
92
|
+
if (
|
93
|
+
self.flurophoreobj.fluorophore.states[final_state_name].state_type
|
94
|
+
== StateType.FLUORESCENT
|
95
|
+
):
|
96
|
+
self.fluorescent_state_history[
|
97
|
+
self.flurophoreobj.fluorophore.states[final_state_name].name
|
98
|
+
][0] += self.time_duration
|
64
99
|
break
|
65
100
|
|
66
101
|
# print(final_state_name)
|