# fdtd - meanas
Electromagnetic simulation tools

Skip to content

# fdtd

## `` meanas.fdtd

Utilities for running finite-difference time-domain (FDTD) simulations

See the discussion of `Maxwell's Equations` in `meanas.fdmath` for basic mathematical background.

### Timestep

From the discussion of "Plane waves and the Dispersion relation" in `meanas.fdmath`, we have

\[ c^2 \Delta_t^2 = \frac{\Delta_t^2}{\mu \epsilon} < 1/(\frac{1}{\Delta_x^2} + \frac{1}{\Delta_y^2} + \frac{1}{\Delta_z^2}) \]

or, if \(\Delta_x = \Delta_y = \Delta_z\), then \(c \Delta_t < \frac{\Delta_x}{\sqrt{3}}\).

Based on this, we can set

    dt = sqrt(mu.min() * epsilon.min()) / sqrt(1/dx_min**2 + 1/dy_min**2 + 1/dz_min**2)

The `dx_min`, `dy_min`, `dz_min` should be the minimum value across both the base and derived grids.

### Poynting Vector and Energy Conservation

Let

\[ \begin{aligned} \tilde{S}_{l, l', \vec{r}} &=& &\tilde{E}_{l, \vec{r}} \otimes \hat{H}_{l', \vec{r} + \frac{1}{2}} \\ &=& &\vec{x} (\tilde{E}^y_{l,m+1,n,p} \hat{H}^z_{l',\vec{r} + \frac{1}{2}} - \tilde{E}^z_{l,m+1,n,p} \hat{H}^y_{l', \vec{r} + \frac{1}{2}}) \\ & &+ &\vec{y} (\tilde{E}^z_{l,m,n+1,p} \hat{H}^x_{l',\vec{r} + \frac{1}{2}} - \tilde{E}^x_{l,m,n+1,p} \hat{H}^z_{l', \vec{r} + \frac{1}{2}}) \\ & &+ &\vec{z} (\tilde{E}^x_{l,m,n,p+1} \hat{H}^y_{l',\vec{r} + \frac{1}{2}} - \tilde{E}^y_{l,m,n,p+1} \hat{H}^z_{l', \vec{r} + \frac{1}{2}}) \end{aligned} \]

where \(\vec{r} = (m, n, p)\) and \(\otimes\) is a modified cross product in which the \(\tilde{E}\) terms are shifted as indicated.

By taking the divergence and rearranging terms, we can show that

\[ \begin{aligned} \hat{\nabla} \cdot \tilde{S}_{l, l', \vec{r}} &= \hat{\nabla} \cdot (\tilde{E}_{l, \vec{r}} \otimes \hat{H}_{l', \vec{r} + \frac{1}{2}}) \\ &= \hat{H}_{l', \vec{r} + \frac{1}{2}} \cdot \tilde{\nabla} \times \tilde{E}_{l, \vec{r}} - \tilde{E}_{l, \vec{r}} \cdot \hat{\nabla} \times \hat{H}_{l', \vec{r} + \frac{1}{2}} \\ &= \hat{H}_{l', \vec{r} + \frac{1}{2}} \cdot (-\tilde{\partial}_t \mu_{\vec{r} + \frac{1}{2}} \hat{H}_{l - \frac{1}{2}, \vec{r} + \frac{1}{2}} - \hat{M}_{l, \vec{r} + \frac{1}{2}}) - \tilde{E}_{l, \vec{r}} \cdot (\hat{\partial}_t \tilde{\epsilon}_{\vec{r}} \tilde{E}_{l'+\frac{1}{2}, \vec{r}} + \tilde{J}_{l', \vec{r}}) \\ &= \hat{H}_{l'} \cdot (-\mu / \Delta_t)(\hat{H}_{l + \frac{1}{2}} - \hat{H}_{l - \frac{1}{2}}) - \tilde{E}_l \cdot (\epsilon / \Delta_t )(\tilde{E}_{l'+\frac{1}{2}} - \tilde{E}_{l'-\frac{1}{2}}) - \hat{H}_{l'} \cdot \hat{M}_{l} - \tilde{E}_l \cdot \tilde{J}_{l'} \\ \end{aligned} \]

where in the last line the spatial subscripts have been dropped to emphasize the time subscripts \(l, l'\), i.e.

\[ \begin{aligned} \tilde{E}_l &= \tilde{E}_{l, \vec{r}} \\ \hat{H}_l &= \tilde{H}_{l, \vec{r} + \frac{1}{2}} \\ \tilde{\epsilon} &= \tilde{\epsilon}_{\vec{r}} \\ \end{aligned} \]

etc. For \(l' = l + \frac{1}{2}\) we get

\[ \begin{aligned} \hat{\nabla} \cdot \tilde{S}_{l, l + \frac{1}{2}} &= \hat{H}_{l + \frac{1}{2}} \cdot (-\mu / \Delta_t)(\hat{H}_{l + \frac{1}{2}} - \hat{H}_{l - \frac{1}{2}}) - \tilde{E}_l \cdot (\epsilon / \Delta_t)(\tilde{E}_{l+1} - \tilde{E}_l) - \hat{H}_{l'} \cdot \hat{M}_l - \tilde{E}_l \cdot \tilde{J}_{l + \frac{1}{2}} \\ &= (-\mu / \Delta_t)(\hat{H}^2_{l + \frac{1}{2}} - \hat{H}_{l + \frac{1}{2}} \cdot \hat{H}_{l - \frac{1}{2}}) - (\epsilon / \Delta_t)(\tilde{E}_{l+1} \cdot \tilde{E}_l - \tilde{E}^2_l) - \hat{H}_{l'} \cdot \hat{M}_l - \tilde{E}_l \cdot \tilde{J}_{l + \frac{1}{2}} \\ &= -(\mu \hat{H}^2_{l + \frac{1}{2}} +\epsilon \tilde{E}_{l+1} \cdot \tilde{E}_l) / \Delta_t \\ +(\mu \hat{H}_{l + \frac{1}{2}} \cdot \hat{H}_{l - \frac{1}{2}} +\epsilon \tilde{E}^2_l) / \Delta_t \\ - \hat{H}_{l+\frac{1}{2}} \cdot \hat{M}_l \\ - \tilde{E}_l \cdot \tilde{J}_{l+\frac{1}{2}} \\ \end{aligned} \]

and for \(l' = l - \frac{1}{2}\),

\[ \begin{aligned} \hat{\nabla} \cdot \tilde{S}_{l, l - \frac{1}{2}} &= (\mu \hat{H}^2_{l - \frac{1}{2}} +\epsilon \tilde{E}_{l-1} \cdot \tilde{E}_l) / \Delta_t \\ -(\mu \hat{H}_{l + \frac{1}{2}} \cdot \hat{H}_{l - \frac{1}{2}} +\epsilon \tilde{E}^2_l) / \Delta_t \\ - \hat{H}_{l-\frac{1}{2}} \cdot \hat{M}_l \\ - \tilde{E}_l \cdot \tilde{J}_{l-\frac{1}{2}} \\ \end{aligned} \]

These two results form the discrete time-domain analogue to Poynting's theorem. They hint at the expressions for the energy, which can be calculated at the same time-index as either the E or H field:

\[ \begin{aligned} U_l &= \epsilon \tilde{E}^2_l + \mu \hat{H}_{l + \frac{1}{2}} \cdot \hat{H}_{l - \frac{1}{2}} \\ U_{l + \frac{1}{2}} &= \epsilon \tilde{E}_l \cdot \tilde{E}_{l + 1} + \mu \hat{H}^2_{l + \frac{1}{2}} \\ \end{aligned} \]

Rewriting the Poynting theorem in terms of the energy expressions,

\[ \begin{aligned} (U_{l+\frac{1}{2}} - U_l) / \Delta_t &= -\hat{\nabla} \cdot \tilde{S}_{l, l + \frac{1}{2}} \\ - \hat{H}_{l+\frac{1}{2}} \cdot \hat{M}_l \\ - \tilde{E}_l \cdot \tilde{J}_{l+\frac{1}{2}} \\ (U_l - U_{l-\frac{1}{2}}) / \Delta_t &= -\hat{\nabla} \cdot \tilde{S}_{l, l - \frac{1}{2}} \\ - \hat{H}_{l-\frac{1}{2}} \cdot \hat{M}_l \\ - \tilde{E}_l \cdot \tilde{J}_{l-\frac{1}{2}} \\ \end{aligned} \]

This result is exact and should practically hold to within numerical precision. No time- or spatial-averaging is necessary.

Note that each value of \(J\) contributes to the energy twice (i.e. once per field update) despite only causing the value of \(E\) to change once (same for \(M\) and \(H\)).

### Sources

It is often useful to excite the simulation with an arbitrary broadband pulse and then extract the frequency-domain response by performing an on-the-fly Fourier transform of the time-domain fields.

`accumulate_phasor` in `meanas.fdtd.phasor` performs the phase accumulation for one or more target frequencies, while leaving source normalization and simulation-loop policy to the caller. `temporal_phasor(...)` and `temporal_phasor_scale(...)` apply the same Fourier sum to a scalar waveform, which is useful when a pulsed source envelope must be normalized before being applied to a point source or mode source. `real_injection_scale(...)` is the corresponding helper for the common real-valued injection pattern `numpy.real(scale * waveform)`. Convenience wrappers `accumulate_phasor_e`, `accumulate_phasor_h`, and `accumulate_phasor_j` apply the usual Yee time offsets. `reconstruct_real(...)` and the corresponding `reconstruct_real_e/h/j(...)` wrappers perform the inverse operation, converting phasors back into real-valued field snapshots at explicit Yee-aligned times. For scalar `omega`, the reconstruction helpers accept either a plain field phasor or the batched `(1, *sample_shape)` form used by the accumulator helpers. The helpers accumulate

\[ \Delta_t \sum_l w_l e^{-i \omega t_l} f_l \]

with caller-provided sample times and weights. In this codebase the matching electric-current convention is typically `E -= dt * J / epsilon` in FDTD and `-i \omega J` on the right-hand side of the FDFD wave equation.

For FDTD/FDFD equivalence, this phasor path is the primary comparison workflow. It isolates the guided `+\omega` response that the frequency-domain solver targets directly, regardless of whether the underlying FDTD run used real- or complex-valued fields.

For exact pulsed FDTD/FDFD equivalence it is often simplest to keep the injected source, fields, and CPML auxiliary state complex-valued. The `real_injection_scale(...)` helper is instead for the more ordinary one-run real-valued source path, where the intended positive-frequency waveform is injected through `numpy.real(scale * waveform)` and any remaining negative- frequency leakage is controlled by the pulse bandwidth and run length.

`reconstruct_real(...)` is for a different question: given a phasor, what late real-valued field snapshot should it produce? That raw-snapshot comparison is stricter and noisier because a monitor plane generally contains both the guided field and the remaining orthogonal content,

\[ E_{\text{monitor}} = E_{\text{guided}} + E_{\text{residual}} . \]

Phasor/modal comparisons mostly validate the guided `+\omega` term. Raw real-field comparisons expose both terms at once, so they should be treated as secondary diagnostics rather than the main solver-equivalence benchmark.

The Ricker wavelet (normalized second derivative of a Gaussian) is commonly used for the pulse shape. It can be written

\[ f_r(t) = (1 - \frac{1}{2} (\omega (t - \tau))^2) e^{-(\frac{\omega (t - \tau)}{2})^2} \]

with \(\tau > \frac{2 * \pi}{\omega}\) as a minimum delay to avoid a discontinuity at t=0 (assuming the source is off for t<0 this gives \(\sim 10^{-3}\) error at t=0).

### Boundary conditions

`meanas.fdtd` exposes two boundary-related building blocks:

- `conducting_boundary(...)` for simple perfect-electric-conductor style field clamping at one face of the domain.
- `cpml_params(...)` and `updates_with_cpml(...)` for convolutional perfectly matched layers (CPMLs) attached to one or more faces of the Yee grid.

`updates_with_cpml(...)` accepts a three-by-two table of CPML parameter blocks:

    cpml_params[axis][polarity_index]

where `axis` is `0`, `1`, or `2` and `polarity_index` corresponds to `(-1, +1)`. Passing `None` for one entry disables CPML on that face while leaving the other directions unchanged. This is how mixed boundary setups such as "absorbing in x, periodic in y/z" are expressed.

When comparing an FDTD run against an FDFD solve, use the same stretched coordinate system in both places:

1. Build the FDTD update with the desired CPML parameters.
2. Stretch the FDFD `dxes` with the matching SCPML transform.
3. Compare the extracted phasor against the FDFD field or residual on those stretched `dxes`.

The electric-current sign convention used throughout the examples and tests is

\[ E \leftarrow E - \Delta_t J / \epsilon \]

which matches the FDFD right-hand side

\[ -i \omega J. \]

## Core update and analysis helpers

## `` meanas.fdtd.base

Basic FDTD field updates

### `` maxwell_e

    maxwell_e(
        dt: float, dxes: dx_lists_t | None = None
    ) -> fdfield_updater_t

Build a function which performs a portion the time-domain E-field update,

    E += curl_back(H[t]) / epsilon

The full update should be

    E += (curl_back(H[t]) + J) / epsilon

which requires an additional step of `E += J / epsilon` which is not performed by the generated function.

See `meanas.fdmath` for descriptions of

- This update step: "Maxwell's equations" section
- `dxes`: "Datastructure: dx_lists_t" section
- `epsilon`: "Permittivity and Permeability" section

Also see the "Timestep" section of `meanas.fdtd` for a discussion of the `dt` parameter.

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`dt` | `float` | Timestep. See `meanas.fdtd` for details. | required
`dxes` | `dx_lists_t | None` | Grid description; see `meanas.fdmath`. | `None`

Returns:

Type | Description
--- | ---
`fdfield_updater_t` | Function `f(E_old, H_old, epsilon) -> E_new`.

### `` maxwell_h

    maxwell_h(
        dt: float, dxes: dx_lists_t | None = None
    ) -> fdfield_updater_t

Build a function which performs part of the time-domain H-field update,

    H -= curl_forward(E[t]) / mu

The full update should be

    H -= (curl_forward(E[t]) + M) / mu

which requires an additional step of `H -= M / mu` which is not performed by the generated function; this step can be omitted if there is no magnetic current `M`.

See `meanas.fdmath` for descriptions of

- This update step: "Maxwell's equations" section
- `dxes`: "Datastructure: dx_lists_t" section
- `mu`: "Permittivity and Permeability" section

Also see the "Timestep" section of `meanas.fdtd` for a discussion of the `dt` parameter.

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`dt` | `float` | Timestep. See `meanas.fdtd` for details. | required
`dxes` | `dx_lists_t | None` | Grid description; see `meanas.fdmath`. | `None`

Returns:

Type | Description
--- | ---
`fdfield_updater_t` | Function `f(E_old, H_old, epsilon) -> E_new`.

## `` meanas.fdtd.pml

Convolutional perfectly matched layer (CPML) support for FDTD updates.

The helpers in this module construct per-face CPML parameters and then wrap the standard Yee updates with the additional auxiliary `psi` fields needed by the CPML recurrence.

The intended call pattern is:

1. Build a `cpml_params[axis][polarity_index]` table with `cpml_params(...)`.
2. Pass that table into `updates_with_cpml(...)` together with `dt`, `dxes`, and `epsilon`.
3. Advance the returned `update_E` / `update_H` closures in the simulation loop.

Each face can be enabled or disabled independently by replacing one table entry with `None`.

### `` cpml_params

    cpml_params(
        axis: int,
        polarity: int,
        dt: float,
        thickness: int = 8,
        ln_R_per_layer: float = -1.6,
        epsilon_eff: float = 1,
        mu_eff: float = 1,
        m: float = 3.5,
        ma: float = 1,
        cfs_alpha: float = 0,
    ) -> dict[str, Any]

Construct the parameter block for one CPML face.

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`axis` | `int` | Which Cartesian axis the CPML is normal to (`0`, `1`, or `2`). | required
`polarity` | `int` | Which face along that axis (`-1` for the low-index face, `+1` for the high-index face). | required
`dt` | `float` | Timestep used by the Yee update. | required
`thickness` | `int` | Number of Yee cells occupied by the CPML region. | `8`
`ln_R_per_layer` | `float` | Logarithmic attenuation target per layer. | `-1.6`
`epsilon_eff` | `float` | Effective permittivity used when choosing the CPML scaling. | `1`
`mu_eff` | `float` | Effective permeability used when choosing the CPML scaling. | `1`
`m` | `float` | Polynomial grading exponent for `sigma` and `kappa`. | `3.5`
`ma` | `float` | Polynomial grading exponent for the complex-frequency shift `alpha`. | `1`
`cfs_alpha` | `float` | Maximum complex-frequency shift parameter. | `0`

Returns:

Type | Description
--- | ---
`dict[str, Any]` | Dictionary with:

`dict[str, Any]`

- `param_e`: `(p0, p1, p2)` arrays for the E update

`dict[str, Any]`

- `param_h`: `(p0, p1, p2)` arrays for the H update

`dict[str, Any]`

- `region`: slice tuple selecting the CPML cells on that face

### `` updates_with_cpml

    updates_with_cpml(
        cpml_params: Sequence[Sequence[dict[str, Any] | None]],
        dt: float,
        dxes: dx_lists_t,
        epsilon: fdfield,
        *,
        dtype: DTypeLike = numpy.float32,
    ) -> tuple[Callable[..., None], Callable[..., None]]

Build Yee-step update closures augmented with CPML terms.

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`cpml_params` | `Sequence[Sequence[dict[str, Any] | None]]` | Three-by-two sequence indexed as `[axis][polarity_index]`. Entries are the dictionaries returned by `cpml_params(...)`; use `None` to disable CPML on one face. | required
`dt` | `float` | Timestep. | required
`dxes` | `dx_lists_t` | Yee-grid spacing lists `[dx_e, dx_h]`. | required
`epsilon` | `fdfield` | Electric material distribution used by the E update. | required
`dtype` | `DTypeLike` | Storage dtype for the auxiliary CPML state arrays. | `float32`

Returns:

Type | Description
--- | ---
`Callable[..., None]` | `(update_E, update_H)` closures with the same call shape as the basic
`Callable[..., None]` | Yee updates:

`tuple[Callable[..., None], Callable[..., None]]`

- `update_E(e, h, epsilon)`

`tuple[Callable[..., None], Callable[..., None]]`

- `update_H(e, h, mu)`

`tuple[Callable[..., None], Callable[..., None]]` | The closures retain the CPML auxiliary state internally.

## `` meanas.fdtd.boundaries

Boundary conditions

### TODO conducting boundary documentation

## `` meanas.fdtd.energy

### `` poynting

    poynting(
        e: fdfield, h: fdfield, dxes: dx_lists_t | None = None
    ) -> fdfield_t

Calculate the poynting vector `S` (\(S\)).

This is the energy transfer rate (amount of energy `U` per `dt` transferred between adjacent cells) in each direction that happens during the half-step bounded by the two provided fields.

The returned vector field `S` is the energy flow across +x, +y, and +z boundaries of the corresponding `U` cell. For example,

        mx = numpy.roll(mask, -1, axis=0)
        my = numpy.roll(mask, -1, axis=1)
        mz = numpy.roll(mask, -1, axis=2)

        u_hstep = fdtd.energy_hstep(e0=es[ii - 1], h1=hs[ii], e2=es[ii],     **args)
        u_estep = fdtd.energy_estep(h0=hs[ii],     e1=es[ii], h2=hs[ii + 1], **args)
        delta_j_B = fdtd.delta_energy_j(j0=js[ii], e1=es[ii], dxes=dxes)
        du_half_h2e = u_estep - u_hstep - delta_j_B

        s_h2e = -fdtd.poynting(e=es[ii], h=hs[ii], dxes=dxes) * dt
        planes = [s_h2e[0, mask].sum(), -s_h2e[0, mx].sum(),
                  s_h2e[1, mask].sum(), -s_h2e[1, my].sum(),
                  s_h2e[2, mask].sum(), -s_h2e[2, mz].sum()]

        assert_close(sum(planes), du_half_h2e[mask])

(see `meanas.tests.test_fdtd.test_poynting_planes`)

The full relationship is

\[ \begin{aligned} (U_{l+\frac{1}{2}} - U_l) / \Delta_t &= -\hat{\nabla} \cdot \tilde{S}_{l, l + \frac{1}{2}} \\ - \hat{H}_{l+\frac{1}{2}} \cdot \hat{M}_l \\ - \tilde{E}_l \cdot \tilde{J}_{l+\frac{1}{2}} \\ (U_l - U_{l-\frac{1}{2}}) / \Delta_t &= -\hat{\nabla} \cdot \tilde{S}_{l, l - \frac{1}{2}} \\ - \hat{H}_{l-\frac{1}{2}} \cdot \hat{M}_l \\ - \tilde{E}_l \cdot \tilde{J}_{l-\frac{1}{2}} \\ \end{aligned} \]

These equalities are exact and should practically hold to within numerical precision. No time- or spatial-averaging is necessary. (See `meanas.fdtd` docs for derivation.)

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`e` | `fdfield` | E-field | required
`h` | `fdfield` | H-field (one half-timestep before or after `e`) | required
`dxes` | `dx_lists_t | None` | Grid description; see `meanas.fdmath`. | `None`

Returns:

Name | Type | Description
--- | --- | ---
`s` | `fdfield_t` | Vector field. Components indicate the energy transfer rate from the corresponding energy cell into its +x, +y, and +z neighbors during the half-step from the time of the earlier input field until the time of later input field.

### `` poynting_divergence

    poynting_divergence(
        s: fdfield | None = None,
        *,
        e: fdfield | None = None,
        h: fdfield | None = None,
        dxes: dx_lists_t | None = None,
    ) -> fdfield_t

Calculate the divergence of the poynting vector.

This is the net energy flow for each cell, i.e. the change in energy `U` per `dt` caused by transfer of energy to nearby cells (rather than absorption/emission by currents `J` or `M`).

See `poynting` and `meanas.fdtd` for more details. Args: s: Poynting vector, as calculated with `poynting`. Optional; caller can provide `e` and `h` instead. e: E-field (optional; need either `s` or both `e` and `h`) h: H-field (optional; need either `s` or both `e` and `h`) dxes: Grid description; see `meanas.fdmath`.

Returns:

Name | Type | Description
--- | --- | ---
`ds` | `fdfield_t` | Divergence of the poynting vector. Entries indicate the net energy flow out of the corresponding energy cell.

### `` energy_hstep

    energy_hstep(
        e0: fdfield,
        h1: fdfield,
        e2: fdfield,
        epsilon: fdfield | None = None,
        mu: fdfield | None = None,
        dxes: dx_lists_t | None = None,
    ) -> fdfield_t

Calculate energy `U` at the time of the provided H-field `h1`.

TODO: Figure out what this means spatially.

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`e0` | `fdfield` | E-field one half-timestep before the energy. | required
`h1` | `fdfield` | H-field (at the same timestep as the energy). | required
`e2` | `fdfield` | E-field one half-timestep after the energy. | required
`epsilon` | `fdfield | None` | Dielectric constant distribution. | `None`
`mu` | `fdfield | None` | Magnetic permeability distribution. | `None`
`dxes` | `dx_lists_t | None` | Grid description; see `meanas.fdmath`. | `None`

Returns:

Type | Description
--- | ---
`fdfield_t` | Energy, at the time of the H-field `h1`.

### `` energy_estep

    energy_estep(
        h0: fdfield,
        e1: fdfield,
        h2: fdfield,
        epsilon: fdfield | None = None,
        mu: fdfield | None = None,
        dxes: dx_lists_t | None = None,
    ) -> fdfield_t

Calculate energy `U` at the time of the provided E-field `e1`.

TODO: Figure out what this means spatially.

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`h0` | `fdfield` | H-field one half-timestep before the energy. | required
`e1` | `fdfield` | E-field (at the same timestep as the energy). | required
`h2` | `fdfield` | H-field one half-timestep after the energy. | required
`epsilon` | `fdfield | None` | Dielectric constant distribution. | `None`
`mu` | `fdfield | None` | Magnetic permeability distribution. | `None`
`dxes` | `dx_lists_t | None` | Grid description; see `meanas.fdmath`. | `None`

Returns:

Type | Description
--- | ---
`fdfield_t` | Energy, at the time of the E-field `e1`.

### `` delta_energy_h2e

    delta_energy_h2e(
        dt: float,
        e0: fdfield,
        h1: fdfield,
        e2: fdfield,
        h3: fdfield,
        epsilon: fdfield | None = None,
        mu: fdfield | None = None,
        dxes: dx_lists_t | None = None,
    ) -> fdfield_t

Change in energy during the half-step from `h1` to `e2`.

This is just from (e2 * e2 + h3 * h1) - (h1 * h1 + e0 * e2)

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`e0` | `fdfield` | E-field one half-timestep before the start of the energy delta. | required
`h1` | `fdfield` | H-field at the start of the energy delta. | required
`e2` | `fdfield` | E-field at the end of the energy delta (one half-timestep after `h1`). | required
`h3` | `fdfield` | H-field one half-timestep after the end of the energy delta. | required
`epsilon` | `fdfield | None` | Dielectric constant distribution. | `None`
`mu` | `fdfield | None` | Magnetic permeability distribution. | `None`
`dxes` | `dx_lists_t | None` | Grid description; see `meanas.fdmath`. | `None`

Returns:

Type | Description
--- | ---
`fdfield_t` | Change in energy from the time of `h1` to the time of `e2`.

### `` delta_energy_e2h

    delta_energy_e2h(
        dt: float,
        h0: fdfield,
        e1: fdfield,
        h2: fdfield,
        e3: fdfield,
        epsilon: fdfield | None = None,
        mu: fdfield | None = None,
        dxes: dx_lists_t | None = None,
    ) -> fdfield_t

Change in energy during the half-step from `e1` to `h2`.

This is just from (h2 * h2 + e3 * e1) - (e1 * e1 + h0 * h2)

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`h0` | `fdfield` | E-field one half-timestep before the start of the energy delta. | required
`e1` | `fdfield` | H-field at the start of the energy delta. | required
`h2` | `fdfield` | E-field at the end of the energy delta (one half-timestep after `e1`). | required
`e3` | `fdfield` | H-field one half-timestep after the end of the energy delta. | required
`epsilon` | `fdfield | None` | Dielectric constant distribution. | `None`
`mu` | `fdfield | None` | Magnetic permeability distribution. | `None`
`dxes` | `dx_lists_t | None` | Grid description; see `meanas.fdmath`. | `None`

Returns:

Type | Description
--- | ---
`fdfield_t` | Change in energy from the time of `e1` to the time of `h2`.

### `` delta_energy_j

    delta_energy_j(
        j0: fdfield, e1: fdfield, dxes: dx_lists_t | None = None
    ) -> fdfield_t

Calculate the electric-current work term \(J \cdot E\) on the Yee grid.

This is the source contribution that appears beside the flux divergence in the discrete Poynting identities documented in `meanas.fdtd`.

Note that each value of `J` contributes twice in a full Yee cycle (once per half-step energy balance) even though it directly changes `E` only once.

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`j0` | `fdfield` | Electric-current density sampled at the same half-step as the current work term. | required
`e1` | `fdfield` | Electric field sampled at the matching integer timestep. | required
`dxes` | `dx_lists_t | None` | Grid description; defaults to unit spacing. | `None`

Returns:

Type | Description
--- | ---
`fdfield_t` | Per-cell source-work contribution with the scalar field shape.

### `` dxmul

    dxmul(
        ee: fdfield,
        hh: fdfield,
        epsilon: fdfield | float | None = None,
        mu: fdfield | float | None = None,
        dxes: dx_lists_t | None = None,
    ) -> fdfield_t

Multiply E- and H-like field products by material weights and cell volumes.

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`ee` | `fdfield` | Three-component electric-field product, such as `e0 * e2`. | required
`hh` | `fdfield` | Three-component magnetic-field product, such as `h1 * h1`. | required
`epsilon` | `fdfield | float | None` | Electric material weight; defaults to `1`. | `None`
`mu` | `fdfield | float | None` | Magnetic material weight; defaults to `1`. | `None`
`dxes` | `dx_lists_t | None` | Grid description; defaults to unit spacing. | `None`

Returns:

Type | Description
--- | ---
`fdfield_t` | Scalar field containing the weighted electric plus magnetic contribution
`fdfield_t` | for each Yee cell.

## `` meanas.fdtd.phasor

Helpers for extracting single- or multi-frequency phasors from FDTD samples.

These helpers are intentionally low-level: callers own the accumulator arrays and the sampling policy. The accumulated quantity is

    dt * sum(weight * exp(-1j * omega * t_step) * sample_step)

where `t_step = (step + offset_steps) * dt`.

The usual Yee offsets are:

- `accumulate_phasor_e(..., step=l)` for `E_l`
- `accumulate_phasor_h(..., step=l)` for `H_{l + 1/2}`
- `accumulate_phasor_j(..., step=l)` for `J_{l + 1/2}`

`temporal_phasor(...)` and `temporal_phasor_scale(...)` apply the same Fourier sum to a 1D scalar waveform. They are useful for normalizing a pulsed source before that scalar waveform is applied to a point source or spatial mode source. `real_injection_scale(...)` is a companion helper for the common real-valued injection pattern `numpy.real(scale * waveform)`, where `waveform` is the analytic positive-frequency signal and the injected real current should still produce a desired phasor response. `reconstruct_real(...)` and its `E/H/J` wrappers perform the inverse operation: they turn one or more phasors back into real-valued field snapshots at explicit Yee-aligned sample times. For a scalar target frequency they accept either a plain field phasor or the batched `(1, *sample_shape)` form used elsewhere in this module.

These helpers do not choose warmup/accumulation windows or pulse-envelope normalization. They also do not impose a current sign convention. In this codebase, electric-current injection normally appears as `E -= dt * J / epsilon`, which matches the FDFD right-hand side `-1j * omega * J`.

### `` accumulate_phasor

    accumulate_phasor(
        accumulator: NDArray[complexfloating],
        omegas: float
        | complex
        | Sequence[float | complex]
        | NDArray,
        dt: float,
        sample: ArrayLike,
        step: int,
        *,
        offset_steps: float = 0.0,
        weight: ArrayLike = 1.0,
    ) -> NDArray[numpy.complexfloating]

Add one time-domain sample into a phasor accumulator.

The added quantity is

    dt * weight * exp(-1j * omega * t_step) * sample

where `t_step = (step + offset_steps) * dt`.

Note

This helper already multiplies by `dt`. If the caller's normalization factor was derived from a discrete sum that already includes `dt`, pass `weight / dt` here.

### `` temporal_phasor

    temporal_phasor(
        samples: ArrayLike,
        omegas: float
        | complex
        | Sequence[float | complex]
        | NDArray,
        dt: float,
        *,
        start_step: int = 0,
        offset_steps: float = 0.0,
    ) -> NDArray[numpy.complexfloating]

Fourier-project a 1D temporal waveform onto one or more angular frequencies.

The returned quantity is

    dt * sum(exp(-1j * omega * t_step) * samples[step_index])

where `t_step = (start_step + step_index + offset_steps) * dt`.

### `` temporal_phasor_scale

    temporal_phasor_scale(
        samples: ArrayLike,
        omegas: float
        | complex
        | Sequence[float | complex]
        | NDArray,
        dt: float,
        *,
        start_step: int = 0,
        offset_steps: float = 0.0,
        target: ArrayLike = 1.0,
    ) -> NDArray[numpy.complexfloating]

Return the scalar multiplier that gives a desired temporal phasor response.

The returned scale satisfies

    temporal_phasor(scale * samples, omegas, dt, ...) == target

for each target frequency. The result keeps a leading frequency axis even when `omegas` is scalar.

### `` real_injection_scale

    real_injection_scale(
        samples: ArrayLike,
        omegas: float
        | complex
        | Sequence[float | complex]
        | NDArray,
        dt: float,
        *,
        start_step: int = 0,
        offset_steps: float = 0.0,
        target: ArrayLike = 1.0,
    ) -> NDArray[numpy.complexfloating]

Return the scale for a real-valued injection built from an analytic waveform.

If the time-domain source is applied as

    numpy.real(scale * samples[step])

then the desired positive-frequency phasor is obtained by compensating for the 1/2 factor between the real-valued source and its analytic component:

    scale = 2 * target / temporal_phasor(samples, ...)

This helper normalizes only the intended positive-frequency component. Any residual negative-frequency leakage is controlled by the waveform design and the accumulation window.

### `` reconstruct_real

    reconstruct_real(
        phasors: ArrayLike,
        omegas: float
        | complex
        | Sequence[float | complex]
        | NDArray,
        dt: float,
        step: int,
        *,
        offset_steps: float = 0.0,
    ) -> NDArray[numpy.floating]

Reconstruct a real-valued field snapshot from one or more phasors.

The returned quantity is

    real(phasor * exp(1j * omega * t_step))

where `t_step = (step + offset_steps) * dt`.

For multi-frequency inputs, the leading frequency axis is preserved. For a scalar `omega`, callers may pass either `(1, *sample_shape)` or `sample_shape`; the return shape matches that choice.

### `` accumulate_phasor_e

    accumulate_phasor_e(
        accumulator: NDArray[complexfloating],
        omegas: float
        | complex
        | Sequence[float | complex]
        | NDArray,
        dt: float,
        sample: ArrayLike,
        step: int,
        *,
        weight: ArrayLike = 1.0,
    ) -> NDArray[numpy.complexfloating]

Accumulate an E-field sample taken at integer timestep `step`.

### `` accumulate_phasor_h

    accumulate_phasor_h(
        accumulator: NDArray[complexfloating],
        omegas: float
        | complex
        | Sequence[float | complex]
        | NDArray,
        dt: float,
        sample: ArrayLike,
        step: int,
        *,
        weight: ArrayLike = 1.0,
    ) -> NDArray[numpy.complexfloating]

Accumulate an H-field sample corresponding to `H_{step + 1/2}`.

### `` accumulate_phasor_j

    accumulate_phasor_j(
        accumulator: NDArray[complexfloating],
        omegas: float
        | complex
        | Sequence[float | complex]
        | NDArray,
        dt: float,
        sample: ArrayLike,
        step: int,
        *,
        weight: ArrayLike = 1.0,
    ) -> NDArray[numpy.complexfloating]

Accumulate a current sample corresponding to `J_{step + 1/2}`.

### `` reconstruct_real_e

    reconstruct_real_e(
        phasors: ArrayLike,
        omegas: float
        | complex
        | Sequence[float | complex]
        | NDArray,
        dt: float,
        step: int,
    ) -> NDArray[numpy.floating]

Reconstruct a real E-field snapshot taken at integer timestep `step`.

### `` reconstruct_real_h

    reconstruct_real_h(
        phasors: ArrayLike,
        omegas: float
        | complex
        | Sequence[float | complex]
        | NDArray,
        dt: float,
        step: int,
    ) -> NDArray[numpy.floating]

Reconstruct a real H-field snapshot corresponding to `H_{step + 1/2}`.

### `` reconstruct_real_j

    reconstruct_real_j(
        phasors: ArrayLike,
        omegas: float
        | complex
        | Sequence[float | complex]
        | NDArray,
        dt: float,
        step: int,
    ) -> NDArray[numpy.floating]

Reconstruct a real current snapshot corresponding to `J_{step + 1/2}`.