@@ -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