differt.em.GeometricFieldSolver#

class GeometricFieldSolver(tx_polarization='V', rx_polarization='V', radio_materials=None, tx_wavefront_radii=0.0, interaction_matrices=None)[source]#

Bases: AbstractFieldSolver

Computes fields for TracedPaths.

This solver combines a transmitter excitation, one 2x2 dyadic transition (Jones) matrix per interaction along each path, and a receiver projection, following Geometrical Optics (GO) and the Uniform Theory of Diffraction (UTD), see differt.em.

To support an additional InteractionType, subclass this solver, override the matching *_matrix method (e.g., diffraction_matrix), and add the corresponding member to supported_interaction_types. Both steps are required: the latter is what lets transition_matrices stay jax.jit-compatible, as it decides, from a plain Python (i.e., non-traced) set, which of the *_matrix methods to call, rather than branching on the (traced) contents of TracedPaths.interaction_types.

Solver-specific configuration (tx_polarization, rx_polarization, radio_materials, tx_wavefront_radii) lives on the solver instance itself, as plain attributes – mirroring AbstractPathSolver and its subclasses (e.g., ExhaustivePathTracer) – rather than being passed as keyword arguments at call time; every *_matrix method, transition_matrices, compute_fields, and spreading_factor therefore only take (paths, mesh, frequency) (the per-call data), reading their configuration from self.

Examples

from differt.em import (
    GeometricFieldSolver,
    InteractionType,
    compute_received_fields,
)


class MyFieldSolver(GeometricFieldSolver):
    supported_interaction_types = frozenset({
        InteractionType.REFLECTION,
        InteractionType.DIFFRACTION,
    })

    def diffraction_matrix(
        self, paths, mesh, frequency
    ): ...  # your implementation, using 'self.radio_materials' etc.


fields = compute_received_fields(
    paths, mesh, frequency, solver=MyFieldSolver()
)

Note

Unlike Sionna RT, which only supports a point source (equivalently, a wavefront that is already spherical, with a radius of curvature equal to the geometric distance traveled), this solver supports a non-planar incident wavefront at the transmitter, via the tx_wavefront_radii attribute, used internally by compute_fields, transition_matrices, diffraction_matrix, and spreading_factor (all overridable). It accepts either a single value, for an isotropic (spherical) wavefront, a (rho_s, rho_p) tuple, for a general astigmatic wavefront with unequal principal radii along the s- and p-planes, or None, for a planar wavefront: a chain of flat-mirror reflections and/or transmissions leaves both principal radii unchanged (only the total path length, plus each of rho_s/rho_p, matters, regardless of order), since neither interaction does anything but fold the ray direction (for a flat mirror) or leave it unchanged (for a thin transmitting slab).

An astigmatic tx_wavefront_radii combined with at most one diffraction interaction anywhere along the path is not currently supported (and raises at runtime, via equinox.error_if): computing the diffraction point’s edge-fixed radius of curvature would require tracking the wavefront’s principal-axis orientation along the path, not just its two radii. For a single isotropic radius (or a planar wavefront), the existing behavior is unchanged: the UTD distance parameter and the post-diffraction (cylindrical) spreading factor both use the cumulative path length up to the diffraction point, plus tx_wavefront_radii. This also only tracks the wavefront up to the first diffraction: a path with a diffraction bounce followed by further interactions would, in general, need to track the diffracted wavefront’s curvature onward, since diffraction turns even an initially-spherical wavefront astigmatic; L_i already accepts the general astigmatic case (rho_1_i, rho_2_i, rho_e_i) for whoever wants to extend diffraction_matrix that far. Sionna RT has no equivalent feature to compare against; tx_wavefront_radii’s correctness instead rests on the geometric argument above (also checked against the equivalent of physically moving the transmitter back by tx_wavefront_radii, see the test suite).

When tx_polarization is set to an AbstractAntenna instance, its own AbstractAntenna.wavefront_radii is used instead of tx_wavefront_radii.

Attributes

interaction_matrices

Optional mapping of custom interaction matrix functions.

radio_materials

The mapping of material properties.

rx_polarization

The receiver antenna polarization or pattern.

supported_interaction_types

The interaction types handled by transition_matrices.

tx_polarization

The transmitter antenna polarization or pattern.

tx_wavefront_radii

The radius (or radii) of curvature of the incident wavefront at the transmitter.

Methods

compute_fields(paths, mesh, frequency, *[, ...])

Compute the received complex fields for each path.

diffraction_matrix(paths, mesh, frequency)

Compute the per-bounce diffraction transition matrix, for every bounce.

reflection_matrix(paths, mesh, frequency)

Compute the per-bounce reflection transition matrix, for every bounce.

ris_matrix(paths, mesh, frequency)

Compute the per-bounce RIS (Reconfigurable Intelligent Surface) transition matrix.

scattering_matrix(paths, mesh, frequency)

Compute the per-bounce diffuse scattering transition matrix, for every bounce.

spreading_factor(paths[, mesh, wavefront_radii])

Compute the wavefront spreading factor for each path.

transition_matrices(paths, mesh, frequency)

Compute one transition matrix per bounce, dispatching on the bounce's interaction type.

transmission_matrix(paths, mesh, frequency)

Compute the per-bounce transmission transition matrix, for every bounce.

Detailed documentation

compute_fields(paths, mesh, frequency, *, wavefront_radii=None)[source]#

Compute the received complex fields for each path.

Reads tx_polarization, rx_polarization, radio_materials, and tx_wavefront_radii from self – see GeometricFieldSolver.

Parameters:
Return type:

Complex[Array, "*batch"]

Returns:

The received complex fields.

diffraction_matrix(paths, mesh, frequency)[source]#

Compute the per-bounce diffraction transition matrix, for every bounce.

Implements the Uniform Theory of Diffraction (UTD), via diffraction_coefficients, for a wedge formed by two adjacent mesh faces. The convention for a DIFFRACTION bounce is that TracedPaths.objects holds a flat half-edge index 3 * triangle_index + local_edge_index (local_edge_index being 0, 1, or 2), matching Sionna RT’s own wedge addressing and Mesh.wedge_angles’s (num_triangles, 3) shape — unlike REFLECTION/TRANSMISSION, which index a plain triangle. This (rather than Mesh.diffraction_edges, which deduplicates shared edges via a non-jax.jit-compatible unique) keeps this method jax.jit-compatible.

The wedge-face reflection coefficients (used for the shadow- and reflection-boundary terms) use the same finite-thickness slab model as reflection_matrix, matching Sionna RT’s dielectric-wedge diffraction model.

The transmitter’s non-planar wavefront (tx_wavefront_radii, or the value returned by AbstractAntenna.wavefront_radii when tx_polarization is an AbstractAntenna) only affects a DIFFRACTION bounce that is the first interaction along a path; see the note in GeometricFieldSolver. None (a planar wavefront, the \(\rho_0 \to \infty\) limit) uses the well-known plane-wave-incidence distance parameter formula, rather than evaluating the general one at \(\rho_0 \to \infty\) (which would give a 0/0 division); an astigmatic (rho_s, rho_p) tuple (or a WavefrontState) is instead handled via curvature transport through propagate_wavefront – see spreading_factor.

Parameters:
Return type:

Complex[Array, "*batch order 2 2"]

Returns:

One transition matrix per bounce.

Raises:

ValueError – If the mesh does not contain face materials.

interaction_matrices: Mapping[InteractionType | int, Any] | None = None#

Optional mapping of custom interaction matrix functions.

Keyed by InteractionType (or integer). Overrides or extends the built-in interaction matrix methods (such as reflection_matrix, diffraction_matrix, scattering_matrix, transmission_matrix, ris_matrix) without subclassing.

radio_materials: Mapping[str, Material] | None = None#

The mapping of material properties.

Defaults to materials when left to None.

reflection_matrix(paths, mesh, frequency)[source]#

Compute the per-bounce reflection transition matrix, for every bounce.

The reflection coefficients are obtained from reflection_coefficients (or the thin-slab variant thereof, when the material has a finite thickness), then expressed in the local (theta, phi) spherical basis of the incoming and outgoing ray segments.

Parameters:
Return type:

Complex[Array, "*batch order 2 2"]

Returns:

One transition matrix per bounce.

ris_matrix(paths, mesh, frequency)[source]#

Compute the per-bounce RIS (Reconfigurable Intelligent Surface) transition matrix.

Warning

Not implemented yet. Subclasses can override this method to provide custom RIS physics.

Parameters:
Raises:

NotImplementedError – Unconditionally, as RIS modeling is reserved for future implementation.

Return type:

Complex[Array, "*batch order 2 2"]

rx_polarization: Any = 'V'#

The receiver antenna polarization or pattern. See tx_polarization.

scattering_matrix(paths, mesh, frequency)[source]#

Compute the per-bounce diffuse scattering transition matrix, for every bounce.

Implements a Lambertian rough-surface scattering model, following Degli-Esposti’s directive model (as also used by Sionna RT’s RadioMaterial), adapted to DiffeRT’s exact/deterministic path model:

Warning

Sionna RT’s scattering model is defined in terms of a Monte Carlo ray-tube solid angle \(\Omega\), which has no equivalent for a single, deterministic point-to-point path. This method instead uses the solid angle subtended by the scattering triangle, as seen from the next path vertex, \(\mathrm{d}A / s^2\), as a geometrically-motivated substitute (\(\mathrm{d}A\) being the triangle’s area and \(s\) the distance to the next vertex). This is a physically-motivated but distinct adaptation: because Sionna RT’s model is stochastic, this method should not be expected to numerically match its output for a single path, only in a statistical (many-samples) sense.

Given the specular reflection coefficients \(r_s, r_p\) (reflection_matrix, ignoring the scattering-coefficient energy reduction) and the incident and scattered directions \(\hat{k}_i, \hat{k}_o\), the per-polarization scattered amplitude is:

\[a_{s,p} = S \sqrt{f_s(\hat{k}_i, \hat{k}_o, \hat{n}) \frac{\mathrm{d}A}{s^2}} \, |r_{s,p}|,\]

where \(S\) is Material.scattering_coefficient and \(f_s\) is the material’s Material.scattering_pattern (normalized so that its integral over the hemisphere is 1), which defaults to LambertianPattern, \(f_s(\hat{k}_i, \hat{k}_o, \hat{n}) = \max(\hat{n}\cdot\hat{k}_o, 0) / \pi\). A custom (e.g., directive) pattern can be set per-material by subclassing AbstractScatteringPattern. A final rotation mixes the s and p channels according to Material.xpd_coefficient.

Parameters:
Return type:

Complex[Array, "*batch order 2 2"]

Returns:

One transition matrix per bounce.

spreading_factor(paths, mesh=None, *, wavefront_radii=None)[source]#

Compute the wavefront spreading factor for each path.

Reads tx_wavefront_radii (or the value returned by AbstractAntenna.wavefront_radii when tx_polarization is an AbstractAntenna) from self, or uses wavefront_radii if provided explicitly.

For a path with no diffraction interaction, this is the general astigmatic-ray-tube spreading factor, \(1/\sqrt{(\rho_s+L)(\rho_p+L)}\), where \(L\) is the total path length and \(\rho_s\), \(\rho_p\) are the wavefront’s two principal radii of curvature at the transmitter (plus tx_wavefront_radii, for a non-planar source); this is exact for any chain of flat-mirror reflections and/or transmissions, regardless of how many interactions the path has, since neither changes a wavefront’s principal radii (only a flat mirror’s image-method folds the ray direction). If tx_wavefront_radii is None (a planar wavefront), this is instead exactly 1 (a plane wave does not spread at all).

For a path containing a diffraction interaction, the wavefront becomes cylindrical past the diffraction point, and the spreading factor becomes \(1/\sqrt{\rho^i s(\rho^i+s)}\), where \(\rho^i\) is the incident wavefront’s radius of curvature at the diffraction point (the cumulative path length before it, plus tx_wavefront_radii if it is the first interaction) and \(s\) is the path length after it, matching Sionna RT’s model when tx_wavefront_radii is left at its default of 0. For a planar wavefront, this is instead \(1/\sqrt{s}\), the classic cylindrical-spreading law for an edge illuminated by a plane wave (\(\rho^i \to \infty\) independently of \(s\), unlike the amplitude, which would incorrectly vanish if this limit were taken naively). For general astigmatic wavefronts, the spreading factor is evaluated using Kouyoumjian-Pathak / McNamara curvature transport via propagate_wavefront.

Parameters:
Return type:

Float[Array, "*batch"]

Returns:

The spreading factor for each path.

supported_interaction_types: ClassVar[frozenset[InteractionType]] = frozenset({InteractionType.REFLECTION, InteractionType.DIFFRACTION, InteractionType.SCATTERING, InteractionType.TRANSMISSION})#

The interaction types handled by transition_matrices.

Subclasses adding support for a new interaction type must add the corresponding member here, in addition to overriding the matching *_matrix method.

transition_matrices(paths, mesh, frequency)[source]#

Compute one transition matrix per bounce, dispatching on the bounce’s interaction type.

Only interaction types listed in supported_interaction_types are ever dispatched to (a plain Python, non-traced, check), so overriding one of the *_matrix methods without adding the type here has no effect. Any bounce whose interaction type is not covered raises at runtime (works under jax.jit too).

Parameters:
Return type:

Complex[Array, "*batch order 2 2"]

Returns:

One transition matrix per bounce.

transmission_matrix(paths, mesh, frequency)[source]#

Compute the per-bounce transmission transition matrix, for every bounce.

This is a thin-surface transmission model: the transmitting object is treated as a single dielectric slab whose thickness is read from Material.thickness (accounting for multiple internal reflections in closed form, per ITU-R P.2040-3 eq. 43b/44), evaluated at the incidence angle and combined coherently. This matches Sionna RT’s RadioMaterial/ITURadioMaterial transmission model, and, as in Sionna RT, the ray direction is not bent by refraction (the wall is assumed thin enough that the geometric deflection is negligible); only the field amplitude/phase are affected.

This is a deliberate simplification, not an architectural constraint: a volumetric transmission model, where the effective thickness is instead derived from where a ray enters and exits a solid object’s geometry (rather than from a fixed per-material property), can be added later as a sibling implementation, by overriding this method in a subclass.

Parameters:
Return type:

Complex[Array, "*batch order 2 2"]

Returns:

One transition matrix per bounce.

tx_polarization: Any = 'V'#

The transmitter antenna polarization or pattern.

Either "V", "H", a Jones vector, or an AbstractAntenna (that provides .fields(...) and .wavefront_radii(...)).

To model different antennas across a scene, pass a single antenna instance whose array fields carry their own batch dimension, broadcastable against the paths’ batch dimensions – either one antenna model shared by every transmitter, or one antenna model per transmitter (AbstractAntenna is a equinox.Module, so this works out of the box).

When this is an AbstractAntenna instance, its AbstractAntenna.wavefront_radii is used instead of tx_wavefront_radii (which only serves as a fallback for a plain polarization string/vector, since those carry no wavefront-curvature information of their own); its frequency is also used by compute_received_fields when no frequency is passed explicitly.

tx_wavefront_radii: Float[ArrayLike, "*#batch"] | tuple[Float[ArrayLike, "*#batch"], Float[ArrayLike, "*#batch"]] | WavefrontState | None = 0.0#

The radius (or radii) of curvature of the incident wavefront at the transmitter.

For a non-planar (near-field) source. This is a distance, and 0 and None are its two opposite limits, not two ways of saying the same thing: 0 (the default) is the near-distance limit, an ideal point source located exactly at the transmitter, matching Sionna RT’s implicit assumption; None is the far-distance limit (\(\rho_0 \to \infty\)), an ideal plane wave, e.g., a source far enough away that its curvature is negligible – see AbstractFarFieldAntenna. Either of those, a single finite value (spherical wavefront), or a (rho_s, rho_p) tuple (astigmatic wavefront, with unequal principal radii along the s- and p-planes) may be passed. Ignored when tx_polarization is an AbstractAntenna instance – see tx_polarization.