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.
@@ -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[Tuple[float, float, float], Callable[[float], np.ndarray]] = (
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
- return self.power(t)
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 calculate_intensity(
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 calculate_intensity_(
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 / 2.0) ** 2)
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) - self.dof / 2) / self.dof
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.1 # 10% of max radius
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
- r = np.sqrt(x_shifted**2 + y_shifted**2)
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 * r**2 / w_z**2) # Gaussian radial profile
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
@@ -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.355
89
- self._norm_sigma_z = self._sigma_z / 2.355
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
- raise ValueError(
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(self, z_val: float) -> NDArray[np.float64]:
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 / self._norm_sigma_xy) ** 2 + (y / self._norm_sigma_xy) ** 2
136
- z_term = (z_val / self._norm_sigma_z) ** 2
137
- psf_at_detector = np.exp(-0.5 * (r_squared + z_term))
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 = 2 * sigma_xy
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].normalize_psf(
172
- self.generator[i].psf_z(z_val=self.position[2]),
173
- mode="sum",
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(self.current_global_time, time)
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)