@@ -330,7 +330,79 @@ def normal_axis(self) -> Axis:
330330 return self .size .index (0.0 )
331331
332332
333- class AbstractModeMonitor (PlanarMonitor , FreqMonitor ):
333+ class AbstractOverlapMonitor (PlanarMonitor , FreqMonitor ):
334+ """:class:`Monitor` that projects fields onto a specified basis and stores overlap amplitudes.
335+
336+ This base is shared by ModeMonitor and Gaussian-overlap monitors.
337+ """
338+
339+ store_fields_direction : Direction = pydantic .Field (
340+ None ,
341+ title = "Store Fields" ,
342+ description = "Propagation direction for the field profiles stored from overlap calculation." ,
343+ )
344+
345+ colocate : bool = pydantic .Field (
346+ True ,
347+ title = "Colocate Fields" ,
348+ description = "Toggle whether fields should be colocated to grid cell boundaries (i.e. primal grid nodes)." ,
349+ )
350+
351+ conjugated_dot_product : bool = pydantic .Field (
352+ True ,
353+ title = "Conjugated Dot Product" ,
354+ description = "Use conjugated or non-conjugated dot product for overlap/decomposition." ,
355+ )
356+
357+ def plot (
358+ self ,
359+ x : Optional [float ] = None ,
360+ y : Optional [float ] = None ,
361+ z : Optional [float ] = None ,
362+ ax : Ax = None ,
363+ ** patch_kwargs ,
364+ ) -> Ax :
365+ """Plot this overlap monitor with an arrow indicating propagation direction."""
366+ ax = super ().plot (x = x , y = y , z = z , ax = ax , ** patch_kwargs )
367+
368+ kwargs_alpha = patch_kwargs .get ("alpha" )
369+ arrow_alpha = ARROW_ALPHA if kwargs_alpha is None else kwargs_alpha
370+
371+ ax = self ._plot_arrow (
372+ x = x ,
373+ y = y ,
374+ z = z ,
375+ ax = ax ,
376+ direction = self ._dir_arrow ,
377+ bend_radius = None ,
378+ bend_axis = None ,
379+ color = ARROW_COLOR_MONITOR ,
380+ alpha = arrow_alpha ,
381+ both_dirs = True ,
382+ )
383+ return ax
384+
385+ @cached_property
386+ def _dir_arrow (self ) -> tuple [float , float , float ]:
387+ """Direction vector from angles used for overlap (children must define angles)."""
388+ theta , phi = self ._angles
389+ dx = np .cos (phi ) * np .sin (theta )
390+ dy = np .sin (phi ) * np .sin (theta )
391+ dz = np .cos (theta )
392+ return self .unpop_axis (dz , (dx , dy ), axis = self .normal_axis )
393+
394+ @property
395+ def _angles (self ) -> tuple [float , float ]:
396+ """Angle tuple (theta, phi) in radians. Children override to supply values."""
397+ return (0.0 , 0.0 )
398+
399+ def _storage_size_solver (self , num_cells : int , tmesh : ArrayFloat1D ) -> int :
400+ """Default size of intermediate data; store all fields on plane for overlap."""
401+ num_sample = len (getattr (self , "freqs" , [0 ]))
402+ return BYTES_COMPLEX * num_cells * num_sample * 6
403+
404+
405+ class AbstractModeMonitor (AbstractOverlapMonitor ):
334406 """:class:`Monitor` that records mode-related data."""
335407
336408 mode_spec : ModeSpec = pydantic .Field (
@@ -393,6 +465,10 @@ def plot(
393465 )
394466 return ax
395467
468+ @cached_property
469+ def _angles (self ) -> tuple [float , float ]:
470+ return (self .mode_spec .angle_theta , self .mode_spec .angle_phi )
471+
396472 @cached_property
397473 def _dir_arrow (self ) -> tuple [float , float , float ]:
398474 """Source direction normal vector in cartesian coordinates."""
@@ -433,6 +509,138 @@ def _storage_size_solver(self, num_cells: int, tmesh: ArrayFloat1D) -> int:
433509 return bytes_single
434510
435511
512+ class AbstractGaussianOverlapMonitor (AbstractOverlapMonitor ):
513+ """:class:`Monitor` that records amplitudes from decomposition onto a Gaussian-like beam.
514+
515+ Common fields and behavior shared by GaussianOverlapMonitor and
516+ AstigmaticGaussianOverlapMonitor.
517+ """
518+
519+ angle_theta : float = pydantic .Field (
520+ 0.0 ,
521+ title = "Polar Angle" ,
522+ description = "Polar angle of propagation direction." ,
523+ units = RADIAN ,
524+ )
525+
526+ angle_phi : float = pydantic .Field (
527+ 0.0 ,
528+ title = "Azimuth Angle" ,
529+ description = "Azimuth angle of propagation direction." ,
530+ units = RADIAN ,
531+ )
532+
533+ pol_angle : float = pydantic .Field (
534+ 0 ,
535+ title = "Polarization Angle" ,
536+ description = "Specifies the angle between the electric field polarization of the "
537+ "source and the plane defined by the injection axis and the propagation axis (rad). "
538+ "``pol_angle=0`` (default) specifies P polarization, "
539+ "while ``pol_angle=np.pi/2`` specifies S polarization. "
540+ "At normal incidence when S and P are undefined, ``pol_angle=0`` defines: "
541+ "- ``Ey`` polarization for propagation along ``x``."
542+ "- ``Ex`` polarization for propagation along ``y``."
543+ "- ``Ex`` polarization for propagation along ``z``." ,
544+ units = RADIAN ,
545+ )
546+
547+ @property
548+ def _angles (self ) -> tuple [float , float ]:
549+ return (self .angle_theta , self .angle_phi )
550+
551+ def storage_size (self , num_cells : int , tmesh : ArrayFloat1D ) -> int :
552+ """Size of monitor storage given the number of points after discretization."""
553+ # store complex amplitudes for +/- directions
554+ num_dirs = 2
555+ return BYTES_COMPLEX * len (self .freqs ) * num_dirs
556+
557+
558+ class GaussianOverlapMonitor (AbstractGaussianOverlapMonitor ):
559+ """:class:`Monitor` that records amplitudes from decomposition onto a Gaussian beam.
560+
561+ Example
562+ -------
563+ >>> gauss = GaussianOverlapMonitor(
564+ ... size=(0,3,3),
565+ ... source_time=pulse,
566+ ... pol_angle=np.pi / 2,
567+ ... waist_radius=1.0)
568+
569+ Notes
570+ --------
571+ If one wants the focus 'in front' of the source, a negative value of ``waist_distance`` is needed.
572+
573+ .. image:: ../../_static/img/beam_waist.png
574+ :width: 30%
575+ :align: center
576+ """
577+
578+ waist_radius : pydantic .PositiveFloat = pydantic .Field (
579+ 1.0 ,
580+ title = "Waist Radius" ,
581+ description = "Radius of the beam at the waist." ,
582+ units = MICROMETER ,
583+ )
584+
585+ waist_distance : float = pydantic .Field (
586+ 0.0 ,
587+ title = "Waist Distance" ,
588+ description = "Distance from the beam waist along the propagation direction. "
589+ "A positive value means the waist is positioned behind the source, considering the propagation direction. "
590+ "For example, for a beam propagating in the ``+`` direction, a positive value of ``beam_distance`` "
591+ "means the beam waist is positioned in the ``-`` direction (behind the source). "
592+ "A negative value means the beam waist is in the ``+`` direction (in front of the source). "
593+ "For an angled source, the distance is defined along the rotated propagation direction." ,
594+ units = MICROMETER ,
595+ )
596+
597+
598+ class AstigmaticGaussianOverlapMonitor (AbstractGaussianOverlapMonitor ):
599+ """:class:`Monitor` that records amplitudes from decomposition onto an astigmatic Gaussian beam.
600+
601+ The simple astigmatic Gaussian distribution allows
602+ both an elliptical intensity profile and different waist locations for the two principal axes
603+ of the ellipse. When equal waist sizes and equal waist distances are specified in the two
604+ directions, this source becomes equivalent to :class:`GaussianBeam`.
605+
606+ Notes
607+ -----
608+
609+ This class implements the simple astigmatic Gaussian beam described in _`[1]`.
610+
611+ **References**:
612+
613+ .. [1] Kochkina et al., Applied Optics, vol. 52, issue 24, 2013.
614+
615+ Example
616+ -------
617+ >>> gauss = AstigmaticGaussianOverlapMonitor(
618+ ... size=(0,3,3),
619+ ... pol_angle=np.pi / 2,
620+ ... waist_sizes=(1.0, 2.0),
621+ ... waist_distances = (3.0, 4.0))
622+ """
623+
624+ waist_sizes : tuple [pydantic .PositiveFloat , pydantic .PositiveFloat ] = pydantic .Field (
625+ (1.0 , 1.0 ),
626+ title = "Waist sizes" ,
627+ description = "Size of the beam at the waist in the local x and y directions." ,
628+ units = MICROMETER ,
629+ )
630+
631+ waist_distances : tuple [float , float ] = pydantic .Field (
632+ (0.0 , 0.0 ),
633+ title = "Waist distances" ,
634+ description = "Distance to the beam waist along the propagation direction "
635+ "for the waist sizes in the local x and y directions. "
636+ "When ``direction`` is ``+`` and ``waist_distances`` are positive, the waist "
637+ "is on the ``-`` side (behind) the source plane. When ``direction`` is ``+`` and "
638+ "``waist_distances`` are negative, the waist is on the ``+`` side (in front) of "
639+ "the source plane." ,
640+ units = MICROMETER ,
641+ )
642+
643+
436644class FieldMonitor (AbstractFieldMonitor , FreqMonitor ):
437645 """:class:`Monitor` that records electromagnetic fields in the frequency domain.
438646
0 commit comments