Skip to content

Far field and diffraction

Near-to-far-field projection from a closed or open monitor box, the fields of a recorded plane colocated on its grid nodes, and grating diffraction-order analysis. The far-field guide walks through a projection and states its measured accuracy.

Projection

far_field

far_field(
    data,
    monitor_name: str,
    *,
    theta_rad: Union[
        None, float, Sequence[float], ndarray
    ] = None,
    phi_rad: Union[float, Sequence[float], ndarray] = 0.0,
    axis: Optional[Axis] = None,
    sign: float = 1.0,
    freqs_hz: Optional[Sequence[float]] = None,
    mesh: bool = True,
    faces: Optional[
        Sequence[Tuple[str, str, float]]
    ] = None,
    colocate: bool = True,
    simulation=None,
    n_medium: Union[
        None, str, float, Sequence[float], ndarray
    ] = None,
    yee: str = "exact",
    dl_um: Optional[float] = None,
    ux: Union[None, float, Sequence[float], ndarray] = None,
    uy: Union[None, float, Sequence[float], ndarray] = None,
    plane_axis: Optional[Axis] = None,
    direction: Optional[str] = None,
    plane_um: Optional[float] = None,
    x_um: Union[
        None, float, Sequence[float], ndarray
    ] = None,
    y_um: Union[
        None, float, Sequence[float], ndarray
    ] = None,
    exact: bool = False,
    r_um: Optional[float] = None,
    origin_um: Optional[Sequence[float]] = None,
) -> FarField

Project a recorded near-field surface onto the far field.

Single-plane (default) or box projection of the tangential E/H recorded by a ProfileMonitor (data[monitor_name]), via the surface-source currents J = n_hat x H, M = -n_hat x E and the radiation integral (see the module docstring). Returns a FarField whose dataset holds the far-field amplitudes E_theta/E_phi and the intensity, radiated power and directivity that follow, labelled by frequency and the grid's axes. The surface radiates into a homogeneous, lossless medium of index n_medium (vacuum by default).

Each recorded component is read at its own Yee point (yee="exact", the default), on a uniform or a graded mesh, and a symmetry-reduced run is mirrored into the full surface first. A single plane needs one sample along its normal and may keep an interval_space stride. The faces of a box must be recorded one cell thick and at every cell: a face of size dl along its normal, centred at (k + 3/4) dl (on a graded mesh, from a quarter of cell k to a quarter of cell k + 1), records the two normal samples the readout brings E and H together from.

Parameters

data: A RunResult (or any mapping) such that data[monitor_name] is the recorded DFT field DataArray, dims ('f','component','z','y','x') (a single-plane slice has a singleton normal axis). For a box pass per-face monitors via faces. A plain mapping of arrays is read in the wire frame (the solver's corner-anchored coordinates); a RunResult hands its planes over through frame.wire_array, so a fitted run's user-frame coordinates are handled, and its manifest gives the cell size (the grid lines of a graded mesh) and the symmetry planes. The medium sampling for simulation= reads the planes in that frame. monitor_name: Key of the near-field monitor in data (single-plane mode), or the name of a FarFieldMonitor: its faces are projected as recorded (faces is then not needed), into the medium the simulation sampled at them unless n_medium is given, mirrored through the symmetry planes it was compiled with, cut where an open box leaves faces out, and with a plane's edge window. The run keeps that recipe (a reloaded result reads it back from the client.json beside the run); without it the faces are found by their names, <name>.x_minus ... <name>.z_plus, with a warning, the projection is into vacuum unless n_medium says otherwise, and the faces are read as recorded (no open edge, no window). theta_rad, phi_rad: Far-field directions (radians). With mesh=True (default) these are 1-D axes meshed into a (n_theta, n_phi) grid (theta_rad slowest); with mesh=False they are paired elementwise. theta_rad is the polar angle from +z, phi_rad the azimuth from +x in the x-y plane. theta and phi are accepted as the earlier spellings. One target grid is given: theta_rad (directions), ux and uy (direction cosines), or plane_um, x_um and y_um (points on a plane). axis: Plane normal "x"/"y"/"z" (single-plane mode). If None, inferred from the monitor's singleton spatial dimension (pass it for a plane recorded one cell thick). sign: Orientation of the outward normal: +1 along +axis (default, i.e. radiating toward larger axis), -1 along -axis. freqs_hz: Frequencies to project (Hz). Defaults to every frequency on the monitor. mesh: Whether theta_rad/phi_rad are meshed (default) or paired. faces: Optional multi-face projection: a sequence of (monitor_name, axis, sign) triples, one per face, summed with each face's own outward normal. When given, monitor_name/axis/ sign are ignored. Each named monitor must be in data. Faces whose in-plane extent is bounded by other faces (a closed box, or a box with a face left out) are read as a box; parallel planes alone are read as open planes. A list of a box with a face left out is read without its open edge: along an axis that carries one face the faces kept are not cut at all, so they run past that face and past the open side alike. For a dipole at the centre of such a box 0.62 um in half width, in index 1.45, that reads the pattern 9.4 and 6.2 % of the peak (within 30 degrees, on exact dipole fields at 20 and 30 cells per wavelength) away from the same box declared with FarFieldMonitor.box(..., exclude=...), which ends them at the declared planes. Declare an open box that way. colocate: True (default): the planes are the engine's Yee-staggered output, as a ProfileMonitor records them. False: synthetic fields whose components already share their sample points; each component is then read at the coordinate given, on one surface, and no cell size is needed. With yee="legacy" it keeps its earlier meaning (average each component to the in-plane node). simulation: The Simulation the data came from. Its symmetry planes say how a symmetry-reduced run is mirrored into the full surface (see yee); a RunResult records them itself, and when both say, they must agree (ValueError: pass the run's own simulation). A plain mapping of arrays from a symmetry-reduced run needs it. Required by n_medium="auto". When given, the medium at each face is sampled half a cell beyond the recorded plane on the side it radiates into, over the face's recorded extent (the sampling diffraction_orders uses). A plane that is not in one homogeneous medium there, sits in a lossy medium, or sits in a medium of another index than the one projected into, gives a UserWarning. The faces of a closed box that do not all sit in one homogeneous medium raise ValueError when n_medium is given or "auto"; with the default they give a UserWarning and the projection stays the vacuum one. n_medium: Index of the homogeneous, lossless, nonmagnetic half-space the surface radiates into (for a closed box, the medium around it). The wavenumber becomes k = n_medium * 2 pi f / c0 and the wave impedance eta = eta0 / n_medium, in the radiation integral, the far-zone fields, the intensity (FarField.dataset["intensity"]), the radiated power and the directivity. One of:

* ``None`` (default): index 1, vacuum, bit for bit the projection
  ``n_medium=1.0`` gives. With ``simulation=`` a face sitting in a
  medium of another index (a plane inside a substrate) gives a
  ``UserWarning`` that names the index; the numbers do not change.
* a real number ``>= 1``, or one per projected frequency (the
  ``freqs_hz`` used): an index, not a permittivity (a silica
  substrate of permittivity 2.1025 is ``n_medium=1.45``). Equal
  values are reported as one float.
* ``"auto"``: the index sampled from ``simulation`` (required). In
  a dispersive (pole) medium that is ``sqrt(Re eps(f))`` at each
  projected frequency, and `FarField.n_medium` is then a
  per-frequency array. A face that crosses structures, a medium
  that is anisotropic, custom or PEC, or a lossy one (a loss tangent
  ``Im eps / Re eps`` above 1e-3 at a projected frequency, from
  damped poles or ``conductivity_s_per_m``) raises ``ValueError``.

A half-space that is not homogeneous (a plane in the cladding over
a substrate, a box that straddles an interface) is outside the
model: its far field needs the layered Green's function. The
readout's errors scale with the cell in medium wavelengths,
``n_medium * k0 * dl``.

yee: How the recorded components are read.

* ``"exact"`` (default): each component at its own Yee point
  (``E_c`` half a cell along ``c``, ``H_c`` half a cell along the
  two other axes), with no averaging. A single plane (or parallel
  planes alone) is read from its first normal sample, J on the H
  plane half a cell above E. The faces of a box are read from their
  two normal samples: the far fields of E brought onto the H plane
  and of H brought onto the E plane are averaged, which cancels the
  imbalance each leaves between the two currents and halves the part
  of the error that depends on where the source sits. A
  second-order error of the interpolation along the normal remains:
  about ``(k dl)^2 / 8`` of the peak in front/back asymmetry; at 25
  cells per wavelength in vacuum on a uniform mesh up to about
  0.80 % with
  the source near a box corner and up to about 1.06 % near a box
  edge, within 2 % wherever the build does not warn. Box faces take
  fourth-order weights at the
  box edges. Refused, naming the face: a box face recorded with
  ``interval_space`` strides (when the cell size is known), a face
  with more than two samples along its normal, and a box whose
  faces do not reach one cell past it in their plane.

  On a graded mesh each sample is placed from the run's grid lines
  (a ``RunResult``'s manifest lists them), a half-offset component
  at the midpoint of its cell; H is brought onto the E plane with
  the two cells' widths as weights, and the box-edge rule runs on
  the samples' true positions (see the module docstring). Every
  number on a uniform mesh is unchanged. The asymmetry can be
  larger than on a uniform mesh: with a region meshed twice as fine
  around a dipole, ending just outside a box face, it reached 1.79 %
  of the peak at 25 cells per wavelength in the background (0.92 %
  at 35). The solver's time step follows the finest cell, so the
  background runs at a lower Courant number and its dispersion
  error grows, as in a dielectric; and the change of cell size just
  outside the face is an inhomogeneity the projection, which
  assumes one homogeneous medium outside the box, does not model.
  Count the cells per wavelength in the background and use at
  least 30, keep every box face at least 0.35 wavelengths from the
  nearest grid line where the cell size changes, along its normal,
  on either side, and keep every source at least 0.1 wavelengths
  from one (a point source along each axis, a launch along its
  normal): end a refinement inside the box with
  that room, or carry it that far past every face, and keep
  sources that far from where it ends. In that range the
  front/back asymmetry of a vacuum dipole box stays within 2 % of
  the peak at 30 background cells per wavelength for refinements 2
  and 3 times as fine as the background (measured up to 0.90 % and
  0.93 %, a dipole near a box edge; higher ratios are not covered),
  falling at about second order for a source fixed in space.
  Outside it no bound is given: a source on or just past the edge
  of a refined region radiates an asymmetric field by itself, up
  to 1.085 % (three times as fine) and 0.92 % (twice) at 30
  background cells. A `FarFieldMonitor` warns
  about both; see the known limitations.

  On a symmetry-reduced run (its manifest, the simulation a
  ``RunResult`` carries, or ``simulation=`` records symmetry planes)
  each face is mirrored at its own Yee points with the sign each
  plane gives the component: a face across a plane must reach it,
  and a box face normal to a plane gains its mirror image, the face
  on the far side, read as the exact mirror of the recorded one.
  Against the full run's box (its far-side faces the mirror of the
  near ones), the pass that brings E onto the H plane agrees to the
  solver's float32 precision. The pass that brings H onto the E
  plane reads the image face at its outer sample, where the full
  run reads its far-side face at the inner one, so it differs by
  the readout's own second-order error, and so does their mean. On
  FDTD dipole boxes at 16 cells per wavelength (three cases, two
  symmetries, uniform and graded meshes) the mean differs by about
  0.1 to 0.4 % of the peak intensity and falls at about second
  order (0.04 to 0.06 % at 32); the H-onto-E pass by 0.26 to
  0.77 %. On
  exact dipole fields the two differ by up to 8.3e-3 and 4.1e-3 at
  20 cells, 9.1e-4 and 4.5e-4 at 40. Neither the reduced nor the
  full run is systematically closer to the exact pattern. A
  mirror-symmetric field gives a pattern mirror-symmetric to
  round-off.

  Where this readout cannot place the samples it falls back to
  ``"legacy"`` with a warning: planes whose cell size cannot be
  told (pass ``dl_um=``), planes of a graded mesh given as plain
  arrays without the run's grid lines (pass the ``RunResult``), and
  box faces recorded with one sample along their normal (a
  ``FutureWarning``: record them one cell thick). Such a fallback
  mirrors a face across a symmetry plane the earlier way (each
  component averaged onto the node, `unfold_symmetry`) but
  cannot add the mirror image of a box face normal to a plane, so a
  symmetry-reduced box that would fall back is refused, naming the
  face and saying what to record. A plain array is
  from a graded mesh when its samples are not evenly spaced, or are
  spaced by other than a whole number of cells; one cut from an
  evenly graded patch cannot be told from a uniform one.
* ``"legacy"``: the earlier readout, which averages each component
  to the in-plane node (``colocate``) and leaves E and H half a cell
  apart along the normal; it reads the first sample of a face
  recorded one cell thick. On a closed box its front/back asymmetry
  is first order in the cell size (0.28 of the peak at 25 cells per
  wavelength for a vacuum dipole). It mirrors a symmetry-reduced
  run only when ``simulation=`` declares the planes, as before, and
  never adds the mirror image of a box face normal to a plane (such
  a box stays open on the far side). Kept for one release; it gives
  a ``FutureWarning``.

dl_um: The uniform cell size the Yee offsets are measured in. By default it is read from a RunResult's manifest, or from a face's two normal samples. A value that disagrees with the manifest, with a face's two normal samples, or with a face's in-plane sample spacing (which must be a whole number of cells) raises ValueError, and so does any value with a graded run (its manifest's grid lines place the samples) or with plain arrays sampled unevenly. On a plain single plane with no run attached nothing tells the cell size, so a dl_um that divides the in-plane spacing is read as a stride: dl_um=dl / 2 reads a plane recorded every cell as one recorded every second cell of half the size. ux, uy: A k-space grid: direction cosines along the two in-plane axes of plane_axis in xyz order (x and y for "z", x and z for "y", y and z for "x"), meshed like theta_rad and phi_rad (mesh), in the half space direction: the direction with those cosines and +-sqrt(1 - ux^2 - uy^2) along plane_axis. They are fractions of the wavenumber in the medium projected into, so ux = m lambda / (n L) is the m-th grating order of a period L. A point with ux^2 + uy^2 > 1 has no direction (only an evanescent wave has that transverse wavevector, and none reaches the far zone): its fields are nan and its intensity 0, as diffraction_orders reports evanescent orders. On a plane recorded over one period of a periodic structure the power of order m is U (lambda / n)^2 / (A cos theta) (W), with U the intensity at its point, A the period's area (m^2) and theta the angle from the plane's normal. To compare with diffraction_orders: its (k1, k2) run along its in_plane_axes, which follow the cyclic order (y and z for a plane normal to x, z and x for y, x and y for z), so on a plane normal to y its k1 is uy and its k2 is ux (times k); and its power carries the period's area in um^2, 1e12 times the W of the formula. So compared, on an FDTD grating the two agree to about 1e-4 (see the docs). plane_axis: The axis normal to a k-space grid's (ux, uy) plane or to a Cartesian grid's plane. A single plane's own normal by default; a surface of several faces needs it. direction: "+" or "-", the half space of a k-space grid along plane_axis: by default a single plane's outward side when plane_axis is its normal, else "+". plane_um, x_um, y_um: A Cartesian grid: points on the plane at plane_um along plane_axis, at x_um and y_um along its two in-plane axes in xyz order (meshed like theta_rad), in the frame of data[name] (a fitted run's user frame). In the far-field approximation each point reads the far-field amplitude of its direction from origin_um, with r_um its distance; points nearer than 2 D^2 / lambda (D twice the surface's farthest reach from origin_um, its diagonal from its centre; lambda in the medium), where the approximation's phase error passes pi / 8, give a warning: pass exact=True there. exact: False (default): the far-field approximation. True: the field the surface's equivalent currents radiate at a finite distance, with the full Green's function of the homogeneous medium (near, intermediate and far terms), at the points of a Cartesian grid, or on the sphere of radius r_um about origin_um for an angle or k-space grid. The FarField holds r e^{-ikr} times each spherical component of E and H (e_r, e_theta, e_phi, h_r, h_theta, h_phi), which tend to the far-field amplitudes as r grows, the difference falling as 1/r; FarField.cartesian gives the field itself, and its dataset["intensity"] is the power per unit solid angle crossing a sphere about origin_um there. It is computed one of two ways, picked automatically:

* the angular spectrum of the plane's tangential E, for a meshed
  Cartesian grid on a plane parallel to one recorded plane sampled
  evenly along both in-plane axes: zero-padded to at least twice
  the span of the plane and the points, the padding doubled until
  doubling it again moves no value by more than 1e-4 of the
  largest, with at most 2^22 padded points per frequency (about
  0.45 GB at that size), and given up early when the moves, shrinking
  at their last rate, cannot reach that within the cap;
* otherwise (a box, an angle or k-space grid, paired points, an
  unevenly sampled plane, or a plane whose spectrum does not settle
  within that padding, as a field still strong at its edges or read
  far away), a direct sum over the recorded samples with the
  far-field projection's quadrature, in blocks of about 2^18
  (point, sample) pairs. Its cost grows as the number of points
  times the number of samples (about a second of CPU per ten
  million pairs). Near the surface's currents it loses accuracy,
  because its kernel grows as ``1/R^3`` between samples. On a
  dipole's exact field in a closed box, one sample spacing out it
  is off by 3 to 6 % of the peak at 20 cells per wavelength, and
  over the box's edges the band grows in spacings on finer meshes
  (one spacing out up to 11 % at 40 cells and 21 % at 80; the
  readout's own error is reached from two spacings at 20 cells,
  three at 40 and four at 80, from two over a face's interior). A
  point within two sample spacings or a tenth of a wavelength of
  the currents, whichever is farther, gives a warning.

On exact fields, clear of that band, both read the field to the
readout's own error: a dipole in a closed box (uniform mesh, 20
and 40 cells per wavelength) to 7.0e-3 and 1.6e-3 of the peak at
1.2 to 20 um from the box centre, the far-field projection's error
at every distance; a beam that has decayed at its plane's edges,
the two ways within 1e-5 of each other and of the beam. On a plane
that cuts the field the two ways are different equivalent sources
(the tangential E alone against J and M) and differ by about the
cut's own error (6.9e-5 of the peak with 1.4e-3 of it at the
plane's edge, where the cut alone is 9.3e-5). A target point
within a box's span, or a Cartesian point not beyond a plane on
the side it radiates into, is refused; on an angle or k-space grid
of a plane the directions that end at or behind it (its grazing
rim) read ``nan``, with intensity 0 and a warning, and a grid with
no direction in front of it is refused. Needs the default
Yee-lattice readout.

r_um: The distance (um) of an exact projection on an angle or k-space grid, from origin_um. origin_um: The point (um, the frame of data[name]) distances are measured from: the centre of an exact projection's sphere and of a Cartesian grid's far-field approximation, by default the centre of the surface's recorded extent (on a mirrored axis the symmetry plane: the full surface's centre). Runs that record the same surface on different grids, or with and without symmetry planes, can default to origins a fraction of a cell apart, which moves the phase e^{-ikr} of the exact amplitudes: pass origin_um to compare runs. Given with far-field amplitudes on an angle or k-space grid, it is also their phase reference (F e^{i k r_hat . o}); without it they keep the phase they always had, referred to the run's corner frame.

Returns

FarField Its dataset labels the projection by frequency and the grid's own axes ((f, theta, phi), (f, ux, uy), the plane's two axes, or (f, point)), with the n_medium projected into; its methods give the gain, cross section, polarization bases and the field at a distance. See FarField.

Examples

A plane recorded inside a silica substrate (index 1.45) below the device, projected into the substrate within 20 degrees of -z::

theta = np.radians(np.linspace(160.0, 180.0, 41))
phi = np.radians(np.arange(0.0, 360.0, 10.0))
ff = far_field(data, "substrate_plane", theta_rad=theta, phi_rad=phi,
               axis="z", sign=-1.0, n_medium=1.45)
u_sub = ff.dataset["intensity"].isel(f=0)    # (theta, phi), W/sr

The same plane on a k-space grid, and a FarFieldMonitor box's exact field on a plane 20 um above its centre::

u = np.linspace(-1.0, 1.0, 201)
ks = far_field(data, "substrate_plane", ux=u, uy=u, axis="z", sign=-1.0,
               n_medium=1.45)              # ks.dataset["intensity"]
xs = np.linspace(-10.0, 10.0, 81)
spot = far_field(data, "ff", plane_axis="z", plane_um=20.0, x_um=xs,
                 y_um=xs, exact=True)
ex = spot.cartesian()["Ex"].isel(f=0)        # (x, y), V/m

FarField dataclass

FarField(
    theta: ndarray,
    phi: ndarray,
    freqs_hz: ndarray,
    e_theta: ndarray,
    e_phi: ndarray,
    n_medium: Union[float, ndarray] = 1.0,
    grid: str = "angle",
    shape: Optional[Tuple[int, int]] = None,
    exact: bool = False,
    r_um: Optional[ndarray] = None,
    origin_um: Optional[Tuple[float, float, float]] = None,
    e_r: Optional[ndarray] = None,
    h_r: Optional[ndarray] = None,
    h_theta: Optional[ndarray] = None,
    h_phi: Optional[ndarray] = None,
    ux: Optional[ndarray] = None,
    uy: Optional[ndarray] = None,
    direction: Optional[str] = None,
    x_um: Optional[ndarray] = None,
    y_um: Optional[ndarray] = None,
    plane_um: Optional[float] = None,
    plane_axis: Optional[str] = None,
    incident_e_v_per_m: Optional[float] = None,
    phase_origin_um: Optional[
        Tuple[float, float, float]
    ] = None,
)

The projected field on a target grid, per frequency.

Labelled results. dataset is the projection as an xarray.Dataset, labelled by frequency and by the grid's own axes:

  • an angle grid (theta_rad, phi_rad): dims ("f", "theta", "phi"), the polar angle from +z and the azimuth from +x in radians;
  • a k-space grid (ux, uy): dims ("f", "ux", "uy"), the direction cosines along the two in-plane axes of plane_axis in xyz order (each coordinate's attrs["axis"] names its axis: x and z on a plane normal to y), with theta, phi and propagating as coordinates over (ux, uy) (nan angles, and False, where a point has no direction; a direction an exact projection of a plane leaves without field stays True, with nan amplitudes and intensity 0, so dataset["e_theta"].notnull() marks the points that carry a field, on every grid). diffraction_orders runs its (k1, k2) in its own cyclic order instead (z and x on a plane normal to y; the same on a plane normal to z), and its order powers carry the period's area in um^2, so they are 1e12 times the watts of U (lambda / n)^2 / (A cos theta) from this intensity;
  • a Cartesian grid (plane_um, x_um, y_um): dims ("f", a, b) with a and b the plane's two in-plane axes in xyz order (("f", "x", "y") on a plane normal to z, ("f", "x", "z") on one normal to y), in um, the plane's position a scalar coordinate named by its normal axis, and theta, phi and r_um (each point's direction and distance from origin_um) as coordinates over the plane;
  • points paired elementwise (mesh=False): dims ("f", "point"), with the grid's coordinates along point.

Along f (Hz) every array carries the vacuum wavelength wlen_um and the index n_medium projected into as coordinates. The data variables are the far-field amplitudes e_theta and e_phi (V, see below), for an exact projection also e_r (V) and h_r, h_theta, h_phi (A), the radiation intensity intensity (W/sr), and on a meshed angle grid radiated_power (W, over f) and directivity. An exact projection, or a Cartesian grid, carries r_um: one distance (a scalar coordinate) on a sphere, one per point on a plane. The labelled values are the arrays of the flat layout below reshaped onto the grid's axes, bit for bit, as read-only views.

The methods gain, cross_section, polarization, power_density and cartesian return labelled results too, renormalize returns a copy read at a distance, and photonhub.viz.plot_far_field_cut and photonhub.viz.plot_far_field_map plot them.

The flat layout (deprecated). The attributes theta, phi, e_theta, e_phi, e_r, h_r, h_theta, h_phi, r_um, ux, uy, x_um, y_um, the properties intensity and propagating, and the methods radiated_power, directivity, reshape and cartesian_fields hold or read the projection as earlier releases returned it: arrays indexed [i_freq, i_point] over a flat list of points (the grid's two axes meshed, the first slowest). They are kept for one release; reading one gives a FutureWarning naming its labelled replacement. Their values are unchanged.

Attributes

theta, phi: The polar and azimuthal angles (radians) of each point, a flat (n_point,) list (deprecated: dataset["theta"], dataset["phi"]). On a k-space grid they are the directions of its points (nan where a point has none, see ux); on a Cartesian grid, the direction of each point seen from origin_um. freqs_hz: 1-D array of frequencies (Hz), the f coordinate of dataset. e_theta, e_phi: Complex far-field AMPLITUDES F = r e^{-i k r} E (the spherical spreading factor e^{i k r}/r removed), shape (n_freq, n_point) (deprecated: dataset["e_theta"], dataset["e_phi"]). For an exact projection (exact) and on a Cartesian grid they are the field at each point, scaled the same way with r its distance from origin_um (r_um): E_theta = e_theta e^{i k r} / r. n_medium: Index of the medium the field was projected into: a float (1.0, the default, is vacuum), or a (n_freq,) array when it varies with frequency. It sets the wave impedance eta = eta0 / n_medium that the intensity and every quantity derived from it use. grid: "angle" (theta_rad, phi_rad), "kspace" (ux, uy) or "cartesian" (points on a plane, x_um, y_um). shape: (n1, n2) of a meshed grid (mesh=True), else None: the sizes of the grid's two dims in dataset. exact: Whether the field is the exact one at a finite distance (the full Green's function, far_field(..., exact=True)) rather than the far-field approximation. r_um, origin_um: The distance of each point from origin_um (um, a flat (n_point,) array; deprecated: dataset["r_um"]) and that origin (um, the frame of data[name]): set for an exact projection, on a Cartesian grid and after renormalize, else None. e_r, h_r, h_theta, h_phi: For an exact projection, the radial E and the three H components at each point, scaled like e_theta; else None (in the far zone E_r = 0 and H = r_hat x E / eta). Deprecated: dataset["e_r"] and so on. ux, uy, direction: On a k-space grid, the direction cosines of each point along the two in-plane axes of plane_axis in xyz order (x and y for plane_axis="z", x and z for "y", y and z for "x"), and the half space, "+" or "-" along plane_axis. A point with ux^2 + uy^2 > 1 has no direction (only an evanescent wave has that transverse wavevector, and it never reaches the far zone): its fields are nan and its intensity 0, as diffraction_orders reports evanescent orders. ux and uy are flat lists (deprecated: dataset["ux"], dataset["uy"]). x_um, y_um, plane_um: On a Cartesian grid, each point's coordinates along the two in-plane axes of plane_axis in xyz order (flat lists; deprecated: the dataset coordinates named by those axes), and the plane's coordinate along it (um, the frame of data[name]). plane_axis: The axis normal to the (ux, uy) or (x, y) plane. incident_e_v_per_m: The amplitude (V/m) of the plane wave a run driven by one TFSFBox launches, recorded by far_field from the run's simulation; None otherwise. cross_section divides by its intensity. phase_origin_um: The point (um, the frame of data[name]) the amplitudes' phase refers to, recorded by far_field: origin_um when it was given or implied (an exact projection, a Cartesian grid), else the corner of the run's frame (Simulation.origin_um; 0 for a plain mapping of arrays). renormalize measures its distance from it. None for a FarField built by hand.

theta instance-attribute

theta: ndarray

phi instance-attribute

phi: ndarray

freqs_hz instance-attribute

freqs_hz: ndarray

e_theta instance-attribute

e_theta: ndarray

e_phi instance-attribute

e_phi: ndarray

n_medium class-attribute instance-attribute

n_medium: Union[float, ndarray] = 1.0

grid class-attribute instance-attribute

grid: str = 'angle'

shape class-attribute instance-attribute

shape: Optional[Tuple[int, int]] = None

exact class-attribute instance-attribute

exact: bool = False

r_um class-attribute instance-attribute

r_um: Optional[ndarray] = None

origin_um class-attribute instance-attribute

origin_um: Optional[Tuple[float, float, float]] = None

e_r class-attribute instance-attribute

e_r: Optional[ndarray] = None

h_r class-attribute instance-attribute

h_r: Optional[ndarray] = None

h_theta class-attribute instance-attribute

h_theta: Optional[ndarray] = None

h_phi class-attribute instance-attribute

h_phi: Optional[ndarray] = None

ux class-attribute instance-attribute

ux: Optional[ndarray] = None

uy class-attribute instance-attribute

uy: Optional[ndarray] = None

direction class-attribute instance-attribute

direction: Optional[str] = None

x_um class-attribute instance-attribute

x_um: Optional[ndarray] = None

y_um class-attribute instance-attribute

y_um: Optional[ndarray] = None

plane_um class-attribute instance-attribute

plane_um: Optional[float] = None

plane_axis class-attribute instance-attribute

plane_axis: Optional[str] = None

incident_e_v_per_m class-attribute instance-attribute

incident_e_v_per_m: Optional[float] = None

phase_origin_um class-attribute instance-attribute

phase_origin_um: Optional[Tuple[float, float, float]] = None

dataset property

dataset: Dataset

The projection as an xarray.Dataset labelled by frequency and the grid's axes (see the class description): the amplitudes e_theta, e_phi (and an exact projection's e_r, h_r, h_theta, h_phi), intensity (W/sr), and on a meshed angle grid radiated_power (W, integrated over the grid with a sin(theta) solid-angle weight) and directivity (4 pi U / P). Select with .sel(f=..., method="nearest") or .sel(phi=0.0); its arrays are read-only views of the projection's own. f keeps the order the frequencies were projected in, which for a FarFieldMonitor is monotonic; frequencies given unsorted or repeated (freqs_hz=) need .sortby("f") or .isel(f=...) instead of method="nearest". radiated_power is meaningful when the grid tiles a contiguous angular region (a full sphere or a hemisphere), and a grid that covers only part of the light makes it, and the directivity, relative to that part. Paired directions (mesh=False) and k-space and Cartesian grids have neither.

propagating property

propagating: ndarray

Which points have a direction, shape (n_point,): every point of an angle or Cartesian grid, the points of a k-space grid with ux^2 + uy^2 <= 1. A point with a direction can still carry no field: in an exact projection of a plane, a direction that ends at or behind it reads nan with intensity 0 while it stays True here. Which points carry a field is ~np.isnan(e_theta) (labelled: dataset["e_theta"].notnull()). Deprecated (the flat layout): dataset["propagating"] on a k-space grid.

intensity property

intensity: ndarray

Radiation intensity U = (|F_theta|^2 + |F_phi|^2)/(2 eta) (W/sr), with eta = eta0 / n_medium the medium's wave impedance, shape (n_freq, n_point): the power per unit solid angle in the far zone. For an exact projection it is r^2 times the radial component of the time-averaged Poynting vector at each point, Re(E_theta H_phi* - E_phi H_theta*) r^2 / 2, the power per unit solid angle crossing a sphere about origin_um there, which tends to the far-zone value as r grows. 0 at a k-space point with no direction, and at a point of an exact projection with no field (a direction that ends at or behind a single plane). Deprecated (the flat layout): dataset["intensity"].

gain

gain(input_power_w=None) -> DataArray

Gain G = 4 pi U / P_in (dimensionless), labelled like dataset's intensity: the intensity against the power the source delivers, so G = e D with e the radiation efficiency (the share of that power that leaves the surface) and D the directivity. Without losses (and a surface that sees all the light) G = D.

input_power_w is the power the source delivers (W): one number, one per projected frequency, or an xarray.DataArray over f read at the projected frequencies, such as the summed outward flux of PowerMonitor planes that close a small box centred on the source (a RunResult returns each plane's in watts). Off centre, such a box's total is off by an amount first order in the cell size times the offset (0.9 % for a dipole 0.13 um off the centre of a box of half width 0.3 um at 30 cells per wavelength): centre it, or check it against a larger box. The projection cannot tell it, so it is required: ValueError without it. A point dipole's Hertzian power eta k^2 |p|^2 / (12 pi) (p its current moment, amplitude times its cell's volume on a uniform mesh) is the power it delivers only alone in a homogeneous lossless medium: nearby structures change it (the local density of states), so measure it there.

cross_section

cross_section(
    incident_intensity_w_per_m2=None,
) -> DataArray

Differential scattering cross section dsigma/dOmega = U / I_inc in um^2/sr, labelled like dataset's intensity: the scattered intensity U (W/sr) against the intensity I_inc (W/m^2) of the incident plane wave. Its integral over the sphere (dataset["radiated_power"] / I_inc on a full angle grid) is the scattering cross section.

incident_intensity_w_per_m2 is I_inc: one number, one per projected frequency, or an xarray.DataArray over f read at the projected frequencies (a PowerMonitor window inside a TFSFBox, divided by its area, for instance). By default it is the declared plane wave's, |E0|^2 / (2 eta) with E0 the amplitude of the run's one TFSFBox (incident_e_v_per_m) and eta the medium's impedance, so on the far-field approximation dsigma/dOmega = (|e_theta|^2 + |e_phi|^2) / |E0|^2. The plane wave the grid carries has cos(k dl / 2) of that intensity in a PowerMonitor (0.8 % less at 25 cells per wavelength), so a cross section divided by an incident flux measured without the scatterer reads that much higher. A projection of anything else (several sources, another kind, a plain mapping of arrays, a result whose simulation is unknown) needs it given: ValueError. The surface must hold scattered field only, as a TFSFBox's outside does.

polarization

polarization(
    basis: str = "ludwig3",
    *,
    reference: Optional[str] = None,
    boresight: Optional[str] = None,
) -> Dataset

The transverse field in a polarization basis, labelled like dataset and scaled like e_theta (V; the intensity in each is |amplitude|^2 / (2 eta)).

  • "ludwig3": co and cross, Ludwig's third definition (the polarization an antenna range measures). About the boresight b with the co-polar reference x' there (y' = b x x')::

    i_co = x' - (r . x') (r + b) / (1 + r . b) i_cross = y' - (r . y') (r + b) / (1 + r . b)

For b = +z and x' = x this is co = cos(phi) E_theta - sin(phi) E_phi and cross = sin(phi) E_theta + cos(phi) E_phi. A Huygens source polarized along x' has no cross-polar field anywhere, and a dipole along x' none in the two principal planes. boresight is "+x" ... "-z": by default "+z" on an angle grid, the grid's own axis and half space on a k-space grid, and the side of the plane seen from origin_um on a Cartesian one. reference is an axis normal to it, by default the first of its in-plane axes in xyz order (x for z, x for y, y for x). The definition has no value at r = -b. For numerical stability, co and cross are nan throughout the antipodal neighborhood 1 + r . b <= 1e-12. A UserWarning identifies the boresight when requested directions enter this neighborhood. Choose a boresight suited to the hemisphere, or partition a full-sphere calculation. * "circular": right and left, (E_theta - i E_phi) / sqrt(2) and (E_theta + i E_phi) / sqrt(2). With the e^{-i omega t} convention and the field leaving along r, right is the field that turns clockwise seen from behind (the IEEE right hand: thumb along r; optics texts such as Born and Wolf call this left-handed), left the other way. A dipole x + i y radiates right-hand circular light toward +z and left-hand toward -z: an Ex dipole with an Ey dipole whose GaussianPulse has phase=-pi/2 (its current sin(omega t) against the Ex dipole's cos(omega t)); phase=+pi/2 turns both hands over.

On an exact projection the radial component e_r is left out (the bases are transverse to r).

power_density

power_density() -> DataArray

The radial power density S = U / r^2 (W/m^2) at each point's distance, labelled like dataset's intensity: the time-averaged Poynting vector along r there, the intensity of the light a detector at that point receives. It needs distances: an exact projection (the exact radial flux at r), a Cartesian grid, or far-field amplitudes given one with renormalize.

cartesian

cartesian() -> Dataset

The field at each point in Cartesian components, Ex, Ey, Ez (V/m) and Hx, Hy, Hz (A/m), labelled like dataset, in the units of the recorded fields: the amplitudes times e^{i k r} / r with k the medium's wavenumber and r the point's distance. An exact projection gives all six components as computed; in the far-field approximation E is transverse and H = r_hat x E / eta. Needs distances, as power_density.

renormalize

renormalize(distance_um: float) -> 'FarField'

The same far-field amplitudes read at distance_um (um): a copy whose points lie that far from the amplitudes' phase reference (phase_origin_um: origin_um when far_field was given one, else the corner of the run's frame; the magnitudes do not depend on it), which the copy records as its origin_um, so that power_density and cartesian give the field there. The amplitudes themselves do not change: the far field falls as 1/r. At a finite distance R the copy's phase is off by about k d^2 / (2 R), with d the distance from the phase reference to the farthest part of the recorded surface (0.025 rad at 1 mm for a surface 2.8 um from the run's corner at lambda = 1 um): give far_field an origin_um at the emitter, so d is the surface's own size, and keep R above 8 d^2 / lambda, where that error is below pi / 8, or project with exact=True. Far-field amplitudes on an angle or k-space grid only: an exact projection does not fall as 1/r (project again with r_um=), and each point of a Cartesian grid has its own distance.

radiated_power

radiated_power() -> ndarray

Total radiated power P_rad = integral U dOmega per frequency (shape (n_freq,)), integrated over the sampled directions with a sin(theta) solid-angle weight via _solid_angle_weights. Deprecated: dataset["radiated_power"] (a meshed angle grid).

This is only meaningful when the directions tile a contiguous angular region (e.g. a full sphere or a hemisphere mesh); for a sparse cut it is a crude quadrature. Used as the directivity normalizer. An angle grid only (ValueError on a k-space or Cartesian grid); for an exact projection it is the power crossing the sphere of radius r_um.

directivity

directivity() -> ndarray

Directivity D = 4 pi U / P_rad (dimensionless), shape (n_freq, n_point), the radiation pattern normalized so its solid-angle average is 1 (for an isotropic radiator D==1 everywhere). An angle grid only, as radiated_power. Deprecated: dataset["directivity"] (a meshed angle grid).

reshape

reshape(
    n_theta: Optional[int] = None,
    n_phi: Optional[int] = None,
) -> Dict[str, ndarray]

Reshape the flat per-direction arrays back to a meshed (n_freq, n_theta, n_phi) grid (the layout produced when theta and phi were passed as 1-D axes and meshed by far_field). Returns a dict with e_theta, e_phi, intensity, directivity, theta (n_theta,) and phi (n_phi,). The sizes default to shape. On a k-space or Cartesian grid the axes are ux, uy or x_um, y_um instead, theta and phi are (n1, n2) arrays, and there is no directivity. An exact projection adds e_r, h_r, h_theta, h_phi and r_um. Deprecated: dataset holds the same arrays on the grid's axes.

cartesian_fields

cartesian_fields() -> Dict[str, ndarray]

The field at each point in Cartesian components, Ex, Ey, Ez (V/m) and Hx, Hy, Hz (A/m), each (n_freq, n_point), in the units of the recorded fields: the amplitudes times e^{i k r} / r with k the medium's wavenumber and r the point's distance r_um. An exact projection gives all six components; on a Cartesian grid in the far-field approximation E is transverse and H = r_hat x E / eta. Needs a distance: ValueError for far-field amplitudes on an angle or k-space grid (project with exact=True and r_um=, or use renormalize). Deprecated: cartesian returns them labelled.

equivalent_currents

equivalent_currents(
    plane_fields: Mapping[str, DataArray],
    *,
    axis: Axis,
    freq_hz: float,
    sign: float = 1.0,
    colocate: bool = True,
) -> Dict[str, ndarray]

Surface equivalent currents J = n_hat x H, M = -n_hat x E on a single plane normal to axis, at one frequency.

sign is the orientation of the outward normal relative to +axis (+1 for an outward normal along +axis, -1 along -axis). colocate (default on) averages each Yee-staggered component to the in-plane cell node before forming J/M, correct for the engine's staggered DFT output; pass False for synthetic already-co-located fields (see the module docstring's "Yee staggering"). Returns a dict with the three Cartesian components of J and M (keys Jx,Jy,Jz,Mx,My,Mz, each a [i_t2, i_t1] complex array) plus the plane geometry t1,t2 (axis names), c1,c2 (their coords, µm), normal_um (the normal position, µm) and normal_axis (axis).

unfold_symmetry

unfold_symmetry(field: DataArray, simulation) -> DataArray

Mirror a plane recorded on a symmetry-reduced domain into the full plane.

A run with Simulation.symmetry solves a fraction of the domain and records only that fraction. This returns the plane the full domain would have recorded: each component is reflected through every symmetry plane that cuts the monitor, with the sign that plane gives it (_mirror_parity), and the mirrored coordinates run negative.

The symmetry planes sit on the domain's minimum faces, at coordinate 0, so the recorded region has to start there; a monitor that stops short of a symmetry plane cannot be unfolded and raises ValueError.

Components that are half a cell off the node along a mirrored axis are averaged onto the node first (they have no sample on the plane itself), so the result is node-centred: pass colocate=False if you hand it to far_field yourself. far_field mirrors a symmetry-reduced run's faces itself, each component at its own Yee point and with no averaging (this function's parities, _mirror_parity); this one serves the plotted, user-frame view of a result and the earlier readout. A run with no symmetry is returned unchanged.

Near fields

colocated_fields

colocated_fields(
    data,
    monitor_name: str,
    *,
    axis: Optional[str] = None,
    freqs_hz: Union[
        None, float, Sequence[float], ndarray
    ] = None,
    simulation=None,
    dl_um: Optional[float] = None,
) -> Dataset

A recorded field plane with E and H on the same points, at their true coordinates.

The solver records each component at its own Yee point (E_c half a cell along c, H_c half a cell along the two other axes), while data[name] labels every component with its cell's base node, so a product of components read at those labels (a Poynting flux E x H*, an intensity, an overlap) is wrong at first order in the cell. This returns the plane with every recorded component on the plane's grid nodes:

  • In the plane, on the recorded base nodes (data[name]'s own coordinates, which are the grid lines, in either order), less the first node along an axis where some component sits half a cell past the node and has no sample before it. Such a component is interpolated linearly onto each node from its two samples either side: weights 1/2 and 1/2 on a uniform mesh, from the two cells' widths on a graded one, from the distances to two samples a stride apart on a plane recorded with interval_space. On an axis the plane spans whole, recorded at every cell, where the run wraps (periodic or Bloch boundaries, from the run's simulation or simulation=; the cell count from the run's manifest or that simulation), the sample before the first node is the last one, a period back, times the Bloch phase e^{-i k L}, and every node is kept. A plane across the period recorded with a stride, or whose cell count is not known, cannot wrap and warns.
  • Along the normal, a plane recorded one cell thick (two samples on neighbouring grid lines: from the quarter point of a cell k to that of cell k + 1, which is size dl centred at (k + 3/4) dl on a uniform mesh; every face of a FarFieldMonitor, on any mesh) is read on one surface, the E plane of its upper sample: tangential E and normal H as recorded there, tangential H and normal E interpolated onto it from the two cells' H planes. That is where a PowerMonitor at that node reads H, and how far_field brings H onto E on a box face. A plane recorded with one sample cannot be moved along its normal: tangential E and normal H sit on its plane, tangential H and normal E half a cell past it (on a graded mesh, the middle of the cell), where the solver recorded them, with a warning when the plane holds both. Each variable's attrs give its position along the normal; a scalar coordinate named by the normal axis is present when all share one surface.

The samples are placed with far_field's readout: from the run's grid lines (a RunResult's manifest lists them, so graded meshes work), and a symmetry-reduced plane that starts on a symmetry plane is first unfolded into the full plane, each component mirrored at its own Yee point with the sign the plane gives it (a plane wholly on the kept side comes back as recorded).

Accuracy. The interpolation is second order: a plane wave of wavenumber k_a along an interpolated axis a is read cos(k_a d / 2) of its value (d the cell), 1 - (k_a d)^2 / 8; on a plane recorded every s cells the node lies half a cell from one sample and s - 1/2 cells from the other, and the error is about 2 s - 1 times larger. On a dipole's exact field 0.3 to 0.6 um from the plane, every component reads within 1.9 % of its peak (one normal sample) and 2.2 % (two) at 20 cells per wavelength, 0.47 % and 0.59 % at 40. It grows quickly as the plane nears a source: for a plane 0.09 um from a dipole, 24 to 34 % (one sample) and 32 to 62 % (two) at 20 cells, 8.0 to 9.7 % and 9.5 to 13 % at 40, over the dipole's orientation and the plane's axis and side.

Against far_field. On a uniform mesh, a plane recorded at every cell with one sample along its normal is read on far_field's own surfaces for a single plane (E on its plane, H half a cell past it). There the colocated tangential fields, taken as the currents J = n x H and M = -n x E and integrated with its quadrature, give its radiation integrals times cos(k r_a d / 2) exactly, r_a the direction's cosine along the axis a each current's field was interpolated along, when the field has died out at the plane's edges: the same total current and first moment, and its amplitude along the plane's normal to round-off. On a plane that wraps across a period it holds at the plane's diffraction orders (k r_a = k_B + 2 pi m / L on each wrapped axis, k_B its Bloch wavevector), the directions a periodic structure radiates into, but not between them, and not for the first moment; on a Bloch plane the normal is not an order (the total current then differs, by 2.8 % at 20 cells per wavelength on an oblique plane wave). On a plane recorded every s cells the total current and first moment are kept but the factor is (s - 1/2)/s e^{i k r_a d/2} + e^{-i k r_a (s - 1/2) d}/(2 s); on a graded mesh the relation holds to second order (along the normal 2.7e-4, 6.6e-5 and 1.7e-5 of the amplitude, every cell cut in two, then four). A plane recorded one cell thick is read on its upper E plane, while far_field reads a single plane from its first sample, so along the normal the two differ by the normal interpolation's factor, (1 - cos(k dl / 2)) / 2, about (k dl)^2 / 16 of the amplitude: 6.15e-3 at 20 cells per wavelength, 1.54e-3 at 40. On a FarFieldMonitor box face the fields are, bit for bit, far_field's H-onto-E pass there brought onto the nodes (far_field averages that pass with its E-onto-H pass). far_field itself reads each component where it was recorded and does not interpolate in the plane.

Power. A product of two colocated components carries the factor twice. On an FDTD dipole in vacuum (uniform mesh, PML 0.5 um, a plane 0.6 um above it spanning the domain, its edges in the PML), the flux 1/2 Re (E x H*) . n summed over the nodes of a plane recorded one cell thick reads 0.46 %, 0.21 % and 0.12 % below a PowerMonitor on the same plane at 20, 30 and 40 cells per wavelength (the monitor pairs each component with the one that shares its Yee point, and also averages H onto the E plane). With one sample, H half a cell above E, each point of E x H* is wrong at first order (4.2 %, 2.8 % and 2.1 % of the peak flux density); summed over a plane that error leaves a term along the plane's edges, which vanishes where the field has died out there or across a periodic cell and remains where the plane is cut or runs into the PML, as here: 2.0 %, 1.3 % and 0.91 % low (1.6 %, 0.96 % and 0.68 % with the PML 1.5 um from the dipole). Record a plane one cell thick for any product of E and H. Those figures hold for a plane whose edges reach the PML or where the field has died out. A plane cut in the open loses about one cell's strip of flux at its edges, first order, since no node precedes a half-offset component's first sample: cut 1 um from the dipole axis, 1.8 %, 1.1 % and 0.78 % low; cut 0.5 um from it, 5.6 %, 3.7 % and 2.8 %. Record such a plane a few cells wider than the region it is integrated over. A plane that spans a periodic cell, recorded at every cell, wraps and keeps every node: one cell thick across a 1.6 um cell, 0.62 %, 0.28 % and 0.16 % low, second order (7.4 %, 4.7 % and 3.5 % when its first row and column were dropped).

Parameters

data: A RunResult, or a mapping of recorded field_dft arrays (in the wire frame, as far_field takes them). monitor_name: A ProfileMonitor plane, a face of a FarFieldMonitor ("<name>.z_plus" ...), or a FarFieldMonitor plane by its own name. A face's dataset carries n_medium, the index its recipe recorded, which far_field projects into, when the recipe is known (the run keeps it, a reloaded result reads it back from client.json, or simulation= gives it). Without it, a surface read by its own name (its face found by name, as far_field finds it) or a face of a box (two or more faces recorded under its prefix) warns that the index is not known; a lone face read by its own name is an ordinary plane. An open box's cut and a plane's edge window are not applied: they weight the projection, not the fields. axis: The plane's normal, "x", "y" or "z". By default the axis with one sample, or a FarFieldMonitor face's own; pass it for a plane recorded one cell thick. freqs_hz: Frequencies to read (Hz): the recorded frequency nearest each. By default every recorded one. simulation: The Simulation the data came from: its symmetry planes (a RunResult records them; both must agree), its boundaries and grid (a periodic axis wraps; a plain mapping takes the cell count from it) and its far-field recipes. Needed for a plain mapping from a symmetry-reduced or periodic run. dl_um: The uniform cell size, for a plain mapping whose cell size nothing else tells (a RunResult's manifest, or a plane's two normal samples). Checked as far_field checks it.

Returns

xarray.Dataset Dims ("f", a, b), with a, b the plane's two in-plane axes in xyz order (("f", "x", "y") for a plane normal to z), their coordinates the nodes in um in the frame of data[name]; wlen_um (vacuum) beside f, and n_medium for a far-field face whose recipe is known. Data variables Ex ... Hz, those the plane recorded (a FarFieldMonitor face records the four tangential ones), in V/m and A/m for the sources as declared (attrs["normalization"] says when the reader could not restore them).

Raises

ValueError For what far_field refuses on a plane, with its wording: more than two samples along the normal, a dl_um that disagrees with the run or the plane, samples off the run's grid lines, symmetry planes that disagree with the run's; and where the readout cannot place the samples (a plain array with no cell size, or from a graded mesh without the run's grid lines), where far_field falls back to its earlier readout with a warning. Also a plane with one sample along an in-plane axis (unless the domain is one cell there), a FarFieldMonitor box named instead of one of its faces, and an empty freqs_hz.

Warns

UserWarning A plane recorded with one sample along its normal that holds both E and H (they are half a cell apart along it), or with two samples that are not neighbours (read from the first); a plane across a periodic or Bloch axis that cannot wrap (recorded with a stride, or its cell count not known), once per plane; a far-field surface or box face whose recipe is not known (no n_medium).

Examples

The power through a plane recorded one cell thick, from the colocated fields, and a map of |E|^2::

ds = colocated_fields(data, "lid", axis="z").isel(f=0)
sz = 0.5 * np.real(ds["Ex"] * np.conj(ds["Hy"]) - ds["Ey"] * np.conj(ds["Hx"]))
power_w = float(sz.integrate(["x", "y"])) * 1e-12      # um^2 -> m^2
e2 = abs(ds["Ex"]) ** 2 + abs(ds["Ey"]) ** 2 + abs(ds["Ez"]) ** 2

Diffraction orders

diffraction_orders

diffraction_orders(
    simulation,
    data,
    monitor,
    *,
    n_medium: Optional[Union[float, ndarray]] = None,
) -> DiffractionOrders

Decompose a recorded periodic DFT plane into grating orders.

Parameters

simulation: The run's Simulation (grid spacing, boundaries, symmetry and, when n_medium is not given, the analytic cross-section all come from it). data: The run's RunResult, or any name -> DataArray mapping, whose coordinates are taken as the wire frame. The plane's position is read through frame.wire_array, so a fitted run's user-frame coordinates are handled. monitor: The full-plane ProfileMonitor (or its name) recording all four tangential components of the plane normal to its zero-size axis. n_medium: Refractive index of the homogeneous region containing the plane: a scalar, or one value per recorded frequency. Default: sampled from the simulation cross-section at the plane (raises if the plane crosses structures); in a dispersive (pole) medium it is sqrt(Re eps(f)) at each recorded frequency, not eps_inf, and DiffractionOrders.n_medium is then that per-frequency array.

Returns

DiffractionOrders

DiffractionOrders dataclass

DiffractionOrders(
    axis: str,
    in_plane_axes: Tuple[str, str],
    freqs_hz: ndarray,
    orders1: ndarray,
    orders2: ndarray,
    k1: ndarray,
    k2: ndarray,
    kn: ndarray,
    propagating: ndarray,
    amp_s_forward: ndarray,
    amp_p_forward: ndarray,
    amp_s_backward: ndarray,
    amp_p_backward: ndarray,
    power_forward: ndarray,
    power_backward: ndarray,
    n_medium: Union[float, ndarray],
    area_um2: float,
    _raw_plane_power: ndarray,
)

Result of diffraction_orders.

Order arrays are indexed (f, m1, m2) with orders1 / orders2 the integer order coordinates (ascending, 0-centred). Amplitudes are the complex FULL E-field amplitudes of each order's s/p wave (engine phasor units); they are NaN for evanescent/grazing orders, where a directional plane-wave amplitude is not defined. Powers are 0 there.

axis instance-attribute

axis: str

in_plane_axes instance-attribute

in_plane_axes: Tuple[str, str]

freqs_hz instance-attribute

freqs_hz: ndarray

orders1 instance-attribute

orders1: ndarray

orders2 instance-attribute

orders2: ndarray

k1 instance-attribute

k1: ndarray

k2 instance-attribute

k2: ndarray

kn instance-attribute

kn: ndarray

propagating instance-attribute

propagating: ndarray

amp_s_forward instance-attribute

amp_s_forward: ndarray

amp_p_forward instance-attribute

amp_p_forward: ndarray

amp_s_backward instance-attribute

amp_s_backward: ndarray

amp_p_backward instance-attribute

amp_p_backward: ndarray

power_forward instance-attribute

power_forward: ndarray

power_backward instance-attribute

power_backward: ndarray

n_medium instance-attribute

n_medium: Union[float, ndarray]

area_um2 instance-attribute

area_um2: float

theta_rad property

theta_rad: ndarray

Polar diffraction angle from the +axis normal, per (f, m1, m2); NaN for non-propagating orders.

phi_rad property

phi_rad: ndarray

Azimuth of the order's in-plane wavevector in the (u1, u2) frame, per (m1, m2) (frequency-independent).

order

order(m1: int, m2: int) -> Dict[str, ndarray]

Everything about one order, per frequency.

total_power

total_power(direction: str = '+') -> ndarray

Sum of propagating order powers per frequency.

net_power

net_power() -> ndarray

Physical net power through the plane toward +axis per frequency: total_power('+') - total_power('-') (counter-propagating interference carries no net Poynting; evanescent-pair tunneling is excluded with the evanescent mask).

staggered_plane_power

staggered_plane_power() -> ndarray

The naive half-cell-staggered Poynting integral of the recorded plane, computed directly in real space (E on-plane against H a half cell off-plane). Per order it equals the directional net power times cos(Re(k_n) d_axis / 2), an UNDER-count at coarse normal sampling, so it serves as an independent bookkeeping cross-check against the order decomposition, not as the physics reference (net_power is that).