1313import pydantic .v1 as pd
1414import xarray as xr
1515from pandas import DataFrame
16- from xarray .core .types import Self
1716
1817from tidy3d .components .base import cached_property , skip_if_fields_missing
1918from tidy3d .components .base_sim .data .monitor_data import AbstractMonitorData
102101AXIAL_RATIO_CAP = 100
103102# At this sampling rate, the computed area of a sphere is within ~1% of the true value.
104103MIN_ANGULAR_SAMPLES_SPHERE = 10
104+ # Threshold for cos(theta) to avoid unphysically large amplitudes near grazing angles
105+ COS_THETA_THRESH = 1e-5
105106
106107
107108class MonitorData (AbstractMonitorData , ABC ):
@@ -2776,8 +2777,10 @@ def radar_cross_section(self) -> DataArray:
27762777 if not np .all (index_k == 0 ):
27772778 raise SetupError ("Can't compute RCS for a lossy background medium." )
27782779
2779- k = self .k [None , None , None , ...]
2780- eta = self .eta [None , None , None , ...]
2780+ n_leading = max (0 , len (self .dims ) - 1 )
2781+ expand_idx = (None ,) * n_leading + (Ellipsis ,)
2782+ k = self .k [expand_idx ]
2783+ eta = self .eta [expand_idx ]
27812784
27822785 if self .is_2d_simulation :
27832786 constant = k ** 2 / (16 * np .pi * eta )
@@ -3307,6 +3310,17 @@ class DiffractionData(AbstractFieldProjectionData):
33073310 to x, P(S) corresponds to ``Ex``(``Ez``) polarization for monitor normal to y, and P(S)
33083311 corresponds to ``Ex``(``Ey``) polarization for monitor normal to z.
33093312
3313+ Note
3314+ ----
3315+
3316+ The power amplitudes per polarization and diffraction order, and correspondingly the power
3317+ per diffraction order, correspond to the power carried by each diffraction order in the
3318+ monitor normal direction. They are not to be confused with power carried by plane waves
3319+ in the propagation direction of each diffraction order, which can be obtained from the
3320+ spherical-coordinate fields which are also stored. The power definition is such that the
3321+ grating efficiency is the recorded power over the input source power, and the direct sum
3322+ over the power in all orders should equal the total power flowing through the monitor.
3323+
33103324
33113325 Example
33123326 -------
@@ -3473,10 +3487,10 @@ def amps(self) -> DataArray:
34733487 """Complex power amplitude in each order for 's' and 'p' polarizations, normalized so that
34743488 the power carried by the wave of that order and polarization equals ``abs(amps)^2``.
34753489 """
3490+ # use a small threshold to avoid blow-up near grazing angles
34763491 cos_theta = np .cos (np .nan_to_num (self .angles [0 ]))
3477-
3478- # will set amplitudes to 0 for glancing or negative angles
3479- cos_theta [cos_theta <= 0 ] = np .inf
3492+ # set amplitudes to 0 for angles with cos(theta) <= COS_THETA_THRESH (glancing or negative)
3493+ cos_theta [cos_theta <= COS_THETA_THRESH ] = np .inf
34803494
34813495 norm = 1.0 / np .sqrt (2.0 * self .eta ) / np .sqrt (cos_theta )
34823496 amp_theta = self .Etheta .values * norm
@@ -3491,10 +3505,15 @@ def amps(self) -> DataArray:
34913505 return DataArray (np .stack ([amp_phi , amp_theta ], axis = 3 ), coords = coords )
34923506
34933507 @property
3494- def power (self ) -> Self :
3508+ def power (self ) -> DataArray :
34953509 """Total power in each order, summed over both polarizations."""
34963510 return (np .abs (self .amps ) ** 2 ).sum (dim = "polarization" )
34973511
3512+ @property
3513+ def radar_cross_section (self ) -> DataArray :
3514+ """Radar cross section in units of incident power."""
3515+ raise ValueError ("RCS is not a well-defined quantity for diffraction data." )
3516+
34983517 @property
34993518 def fields_spherical (self ) -> xr .Dataset :
35003519 """Get all field components in spherical coordinates relative to the monitor's
0 commit comments