Skip to content

Commit 35891bd

Browse files
authored
Updating optics and scatterers for Tutorial Review. (#210)
* first_commit_review * Fixing documentation for parameters and defining new parameters. - Íscat is defined by setting the input/output polarization to circular and adding a phase_shift_correction term. - Documentation for all variables * Update optics.py
1 parent 4b5e023 commit 35891bd

File tree

3 files changed

+196
-23
lines changed

3 files changed

+196
-23
lines changed

deeptrack/holography.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ def get_propagation_matrix(shape, to_z, pixel_size, wavelength, dx=0, dy=0):
2727

2828

2929
class Rescale(Feature):
30-
"""Rescales an optical field by subtracting the real part of the field beofre multiplication.
30+
"""Rescales an optical field by subtracting the real part of the field before multiplication.
3131
3232
Parameters
3333
----------

deeptrack/optics.py

Lines changed: 146 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -353,12 +353,13 @@ def _pupil(
353353

354354
defocus = np.reshape(defocus, (-1, 1, 1))
355355
z_shift = defocus * np.expand_dims(z_shift, axis=0)
356-
356+
#print(self.pupil)
357357
if include_aberration:
358358
pupil = self.pupil
359359
if isinstance(pupil, Feature):
360360

361361
pupil_function = pupil(pupil_function)
362+
362363
elif isinstance(pupil, np.ndarray):
363364
pupil_function *= pupil
364365

@@ -669,6 +670,12 @@ def get(self, illuminated_volume, limits, fields, **kwargs):
669670
include_aberration=True,
670671
**kwargs,
671672
)[0],
673+
self._pupil(
674+
volume.shape[:2],
675+
defocus=[0],
676+
include_aberration=True,
677+
**kwargs,
678+
)[0]
672679
]
673680

674681
pupil_step = np.fft.fftshift(pupils[0])
@@ -677,7 +684,7 @@ def get(self, illuminated_volume, limits, fields, **kwargs):
677684
light_in = self.illumination.resolve(light_in)
678685
light_in = np.fft.fft2(light_in)
679686

680-
K = 2 * np.pi / kwargs["wavelength"]
687+
K = 2 * np.pi / kwargs["wavelength"]*kwargs["refractive_index_medium"]
681688

682689
z = z_limits[1]
683690
for i, z in zip(index_iterator, z_iterator):
@@ -690,14 +697,17 @@ def get(self, illuminated_volume, limits, fields, **kwargs):
690697
light = np.fft.ifft2(light_in)
691698
light_out = light * np.exp(1j * ri_slice * voxel_size[-1] * K)
692699
light_in = np.fft.fft2(light_out)
693-
694-
shifted_pupil = np.fft.fftshift(pupils[-1])
700+
701+
shifted_pupil = np.fft.fftshift(pupils[1])
695702
light_in_focus = light_in * shifted_pupil
696-
703+
#import matplotlib.pyplot as plt
704+
#plt.imshow(light_in_focus.imag)
705+
#plt.show()
697706
if len(fields) > 0:
698707
field = np.sum(fields, axis=0)
699708
light_in_focus += field[..., 0]
700-
709+
shifted_pupil = np.fft.fftshift(pupils[-1])
710+
light_in_focus = light_in_focus * shifted_pupil
701711
# Mask to remove light outside the pupil.
702712
mask = np.abs(shifted_pupil) > 0
703713
light_in_focus = light_in_focus * mask
@@ -724,6 +734,136 @@ def get(self, illuminated_volume, limits, fields, **kwargs):
724734
Holography = Brightfield
725735

726736

737+
class ISCAT(Brightfield):
738+
"""Images coherently illuminated samples using ISCAT.
739+
740+
Images samples by creating a discretized volume, where each pixel
741+
represents the effective refractive index of that pixel. Light is
742+
propagated through the sample iteratively by first propagating the
743+
light in the fourier space, followed by a refractive index correction
744+
in the real space.
745+
746+
Parameters
747+
----------
748+
illumination : Feature
749+
Feature-set resolving the complex field entering the sample. Default
750+
is a field with all values 1.
751+
NA : float
752+
The NA of the limiting aperature.
753+
wavelength : float
754+
The wavelength of the scattered light in meters.
755+
magnification : float
756+
The magnification of the optical system.
757+
resolution : array_like[float (, float, float)]
758+
The distance between pixels in the camera. A third value can be
759+
included to define the resolution in the z-direction.
760+
refractive_index_medium : float
761+
The refractive index of the medium.
762+
padding : array_like[int, int, int, int]
763+
Pads the sample volume with zeros to avoid edge effects.
764+
output_region : array_like[int, int, int, int]
765+
The region of the image to output (x,y,width,height). Default
766+
None returns entire image.
767+
pupil : Feature
768+
A feature-set resolving the pupil function at focus. The feature-set
769+
receive an unaberrated pupil as input.
770+
illumination_angle : float
771+
The angle relative to the optical axis. Default is π radians in ISCAT.
772+
amp_factor : float
773+
The amplitude factor of the field. Default is 1.
774+
The relative amplitude off the illuminating field and the reference field.
775+
776+
"""
777+
778+
def __init__(
779+
self,
780+
illumination_angle = np.pi,
781+
amp_factor = 1,
782+
**kwargs
783+
):
784+
785+
super().__init__(
786+
illumination_angle=illumination_angle,
787+
amp_factor=amp_factor,
788+
input_polarization="circular",
789+
output_polarization="circular",
790+
phase_shift_correction=True,
791+
**kwargs
792+
)
793+
794+
class Darkfield(Brightfield):
795+
"""Images coherently illuminated samples using Darkfield.
796+
797+
Images samples by creating a discretized volume, where each pixel
798+
represents the effective refractive index of that pixel. Light is
799+
propagated through the sample iteratively by first propagating the
800+
light in the fourier space, followed by a refractive index correction
801+
in the real space.
802+
803+
Parameters
804+
----------
805+
illumination : Feature
806+
Feature-set resolving the complex field entering the sample. Default
807+
is a field with all values 1.
808+
NA : float
809+
The NA of the limiting aperature.
810+
wavelength : float
811+
The wavelength of the scattered light in meters.
812+
magnification : float
813+
The magnification of the optical system.
814+
resolution : array_like[float (, float, float)]
815+
The distance between pixels in the camera. A third value can be
816+
included to define the resolution in the z-direction.
817+
refractive_index_medium : float
818+
The refractive index of the medium.
819+
padding : array_like[int, int, int, int]
820+
Pads the sample volume with zeros to avoid edge effects.
821+
output_region : array_like[int, int, int, int]
822+
The region of the image to output (x,y,width,height). Default
823+
None returns entire image.
824+
pupil : Feature
825+
A feature-set resolving the pupil function at focus. The feature-set
826+
receive an unaberrated pupil as input.
827+
illumination_angle : float
828+
The angle relative to the optical axis. Default is π/2 radians.
829+
830+
"""
831+
832+
def __init__(
833+
self,
834+
illumination_angle = np.pi/2,
835+
**kwargs
836+
):
837+
super().__init__(
838+
illumination_angle=illumination_angle,
839+
**kwargs)
840+
841+
#Retrieve get as super
842+
def get(self, illuminated_volume, limits, fields, **kwargs):
843+
"""Retrieve the darkfield image of the illuminated volume.
844+
845+
Parameters
846+
----------
847+
illuminated_volume : array_like
848+
The volume of the sample being illuminated.
849+
limits : array_like
850+
The spatial limits of the volume.
851+
fields : array_like
852+
The fields interacting with the sample.
853+
**kwargs : dict
854+
Additional parameters passed to the super class's get method.
855+
856+
Returns
857+
-------
858+
numpy.ndarray
859+
The darkfield image obtained by calculating the squared absolute
860+
difference from 1.
861+
"""
862+
863+
field = super().get(illuminated_volume, limits, fields, return_field=True, **kwargs)
864+
return np.square(np.abs(field-1))
865+
866+
727867
class IlluminationGradient(Feature):
728868
"""Adds a gradient in the illumination
729869

deeptrack/scatterers.py

Lines changed: 49 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -489,10 +489,12 @@ class MieScatterer(Scatterer):
489489
input_polarization: float or Quantity
490490
Defines the polarization angle of the input. For simulating circularly
491491
polarized light we recommend a coherent sum of two simulated fields. For
492-
unpolarized light we recommend a incoherent sum of two simulated fields.
492+
unpolarized light we recommend a incoherent sum of two simulated fields.
493+
If defined as "circular", the coefficients are set to 1/2.
493494
output_polarization: float or Quantity or None
494495
If None, the output light is not polarized. Otherwise defines the angle of the
495496
polarization filter after the sample. For off-axis, keep the same as input_polarization.
497+
If defined as "circular", the coefficients are multiplied by 1. I.e. no change.
496498
L : int or str
497499
The number of terms used to evaluate the mie theory. If `"auto"`,
498500
it determines the number of terms automatically.
@@ -507,8 +509,16 @@ class MieScatterer(Scatterer):
507509
If True, the feature returns the fft of the field, rather than the
508510
field itself.
509511
coherence_length : float
510-
The temporal coherence length of a partially coherent light given in meters. If None, the illumination is
511-
assumed to be coherent.
512+
The temporal coherence length of a partially coherent light given in meters.
513+
If None, the illumination is assumed to be coherent.
514+
amp_factor : float
515+
A factor that scales the amplification of the field.
516+
This is useful for scaling the field to the correct intensity. Default is 1.
517+
phase_shift_correction : bool
518+
If True, the feature applies a phase shift correction to the output field.
519+
This is necessary for ISCAT simulations.
520+
The correction depends on the k-vector and z according to the formula:
521+
arr*=np.exp(1j * k * z + 1j * np.pi / 2)
512522
"""
513523

514524
__gpu_compatible__ = True
@@ -540,6 +550,9 @@ def __init__(
540550
position_objective=(0, 0),
541551
return_fft=False,
542552
coherence_length=None,
553+
illumination_angle=0,
554+
amp_factor=1,
555+
phase_shift_correction=False,
543556
**kwargs,
544557
):
545558
if polarization_angle is not None:
@@ -569,6 +582,9 @@ def __init__(
569582
position_objective=position_objective,
570583
return_fft=return_fft,
571584
coherence_length=coherence_length,
585+
illumination_angle=illumination_angle,
586+
amp_factor=amp_factor,
587+
phase_shift_correction=phase_shift_correction,
572588
**kwargs,
573589
)
574590

@@ -617,7 +633,7 @@ def get_XY(self, shape, voxel_size):
617633
def get_detector_mask(self, X, Y, radius):
618634
return np.sqrt(X**2 + Y**2) < radius
619635

620-
def get_plane_in_polar_coords(self, shape, voxel_size, plane_position):
636+
def get_plane_in_polar_coords(self, shape, voxel_size, plane_position, illumination_angle):
621637

622638
X, Y = self.get_XY(shape, voxel_size)
623639
X = image.maybe_cupy(X)
@@ -633,9 +649,10 @@ def get_plane_in_polar_coords(self, shape, voxel_size, plane_position):
633649

634650
# get the angles
635651
cos_theta = Z / R3
652+
illumination_cos_theta=np.cos(np.arccos(cos_theta)+illumination_angle)
636653
phi = np.arctan2(Y, X)
637654

638-
return R3, cos_theta, phi
655+
return R3, cos_theta, illumination_cos_theta, phi
639656

640657
def get(
641658
self,
@@ -657,9 +674,11 @@ def get(
657674
return_fft,
658675
coherence_length,
659676
output_region,
677+
illumination_angle,
678+
amp_factor,
679+
phase_shift_correction,
660680
**kwargs,
661681
):
662-
663682
# Get size of the output
664683
xSize, ySize = self.get_xy_size(output_region, padding)
665684
voxel_size = get_active_voxel_size()
@@ -683,9 +702,10 @@ def get(
683702
)
684703

685704
# get field evaluation plane at offset_z
686-
R3_field, cos_theta_field, phi_field = self.get_plane_in_polar_coords(
687-
arr.shape, voxel_size, relative_position * ratio
705+
R3_field, cos_theta_field, illumination_angle_field, phi_field = self.get_plane_in_polar_coords(
706+
arr.shape, voxel_size, relative_position * ratio, illumination_angle
688707
)
708+
689709
cos_phi_field, sin_phi_field = np.cos(phi_field), np.sin(phi_field)
690710
# x and y position of a beam passing through field evaluation plane on the objective
691711
x_farfield = (
@@ -706,43 +726,55 @@ def get(
706726
cos_theta_field = cos_theta_field[pupil_mask]
707727
phi_field = phi_field[pupil_mask]
708728

709-
if isinstance(input_polarization, (float, int, Quantity)):
710-
729+
illumination_angle_field=illumination_angle_field[pupil_mask]
730+
731+
if isinstance(input_polarization, (float, int, str, Quantity)):
711732
if isinstance(input_polarization, Quantity):
712733
input_polarization = input_polarization.to("rad")
713734
input_polarization = input_polarization.magnitude
714735

715-
S1_coef = np.sin(phi_field + input_polarization)
716-
S2_coef = np.cos(phi_field + input_polarization)
736+
if isinstance(input_polarization, (float, int)):
737+
S1_coef = np.sin(phi_field + input_polarization)
738+
S2_coef = np.cos(phi_field + input_polarization)
739+
740+
# If the input polarization is circular set the coefficients to 1/2.
741+
elif isinstance(input_polarization, (str)):
742+
if input_polarization == "circular":
743+
S1_coef = 1/2
744+
S2_coef = 1/2
717745

718746
if isinstance(output_polarization, (float, int, Quantity)):
719747
if isinstance(input_polarization, Quantity):
720748
output_polarization = output_polarization.to("rad")
721749
output_polarization = output_polarization.magnitude
722750

723751
S1_coef *= np.sin(phi_field + output_polarization)
724-
S2_coef *= np.cos(phi_field + output_polarization)
752+
S2_coef *= np.cos(phi_field + output_polarization) * illumination_angle_field
725753

726754
# Wave vector
727755
k = 2 * np.pi / wavelength * refractive_index_medium
728756

729757
# Harmonics
730758
A, B = coefficients(L)
731-
PI, TAU = D.mie_harmonics(cos_theta_field, L)
759+
PI, TAU = D.mie_harmonics(illumination_angle_field, L)
732760

733761
# Normalization factor
734762
E = [(2 * i + 1) / (i * (i + 1)) for i in range(1, L + 1)]
735763

736764
# Scattering terms
737765
S1 = sum([E[i] * A[i] * PI[i] + E[i] * B[i] * TAU[i] for i in range(0, L)])
738766
S2 = sum([E[i] * B[i] * PI[i] + E[i] * A[i] * TAU[i] for i in range(0, L)])
739-
767+
740768
arr[pupil_mask] = (
741769
-1j
742770
/ (k * R3_field)
743771
* np.exp(1j * k * R3_field)
744772
* (S2 * S2_coef + S1 * S1_coef)
745-
)
773+
) / amp_factor
774+
775+
# For phase shift correction (a multiplication of the field by exp(1j * k * z)).
776+
if phase_shift_correction:
777+
arr *= np.exp(1j * k * z + 1j * np.pi / 2)
746778

747779
# For partially coherent illumination
748780
if coherence_length:
@@ -778,6 +810,7 @@ def get(
778810
),
779811
)
780812
fourier_field = fourier_field * propagation_matrix * np.exp(-1j * k * offset_z)
813+
781814
if return_fft:
782815
return fourier_field[..., np.newaxis]
783816
else:

0 commit comments

Comments
 (0)