differt.rt.fermat_path_on_linear_objects

differt.rt.fermat_path_on_linear_objects#

fermat_path_on_linear_objects(from_vertices, to_vertices, object_origins, object_vectors, *, steps=10, unroll=1, linesearch_steps=1, unroll_linesearch=1, implicit_diff=True)[source]#

Return the ray paths between pairs of vertices, that reflect or diffract on a given list of objects in between.

Linear objects are defined by a linear combination of zero or more (possibly orthogonal) vectors, plus a point in space (i.e., the origin).

E.g., a straight line can be defined by a point and a direction vector, while a plane can be defined by a point and two orthogonal direction vectors.

Because the size of object_vectors is determined by the object with the most dimensions, objects with fewer dimensions should have the extra vectors set to zero.

Based on the Fermat principle, this method finds the ray paths between the from and to vertices, that minimize the total length of the paths.

While the method assumes that the objects are infinite, as for the image method, choosing an appropriate origin can be important as it will be used as the initial point of the minimization procedure.

Important

The current implementation uses the fpt-jax library [4], which defines jax.custom_vjp functions for more efficient gradient computations, see the paper for more details. The implementation is not guaranteed to converge for all configurations of vertices and objects, and the user may need to tune the number of steps to achieve convergence.

Parameters:
  • from_vertices (Float[ArrayLike, '*#batch 3']) – An array of from vertices, i.e., vertices from which the ray paths start. In a radio communications context, this is usually an array of transmitters.

  • to_vertices (Float[ArrayLike, '*#batch 3']) – An array of to vertices, i.e., vertices to which the ray paths end. In a radio communications context, this is usually an array of receivers.

  • object_origins (Float[ArrayLike, '*#batch num_objects 3']) –

    An array of object origins.

    It is used as the initial guess of the minimization procedure.

  • object_vectors (Float[ArrayLike, '*#batch num_objects num_dims 3']) – An array of base vectors describing the objects.

  • steps (int) – The number of optimization steps to perform.

  • unroll (int | bool) – Whether to unroll the optimization loop. Can be a boolean or an integer specifying the number of iterations to unroll, see jax.lax.scan.

  • linesearch_steps (int) – The number of line search steps to perform at each iteration.

  • unroll_linesearch (int | bool) – Whether to unroll the line search loop. Can be a boolean or an integer specifying the number of iterations to unroll, see jax.lax.scan.

  • implicit_diff (bool) – Whether to use implicit differentiation for computing the gradient. See [4] and its GitHub page for more details.

Return type:

Float[Array, '*batch num_objects 3']

Returns:

An array of ray paths obtained based on Fermat’s principle.

Note

The paths do not contain the starting and ending vertices.

You can easily create the complete ray paths using assemble_paths:

paths = fermat_path_on_linear_objects(...)

full_paths = assemble_paths(
    from_vertices,
    paths,
    to_vertices,
)

Examples

The following example shows how to use this method to find the ray paths undergoing a diffraction on the edge of a wall, then a reflection on a mirror, before reaching the receiver.

>>> from differt.geometry import TriangleMesh, normalize, assemble_paths
>>> from differt.plotting import draw_markers, draw_paths, reuse
>>> from differt.rt import fermat_path_on_linear_objects
>>>
>>> from_vertex = jnp.array([-2.0, 0.0, 0.0])
>>> to_vertex = jnp.array([0.0, 0.0, 0.0])
>>> wall = TriangleMesh.plane(
...     jnp.array([-1.0, 0.0, 0.0]), normal=jnp.array([1.0, 0.0, 0.0])
... )
>>> mirror = TriangleMesh.plane(
...     jnp.array([1.0, 0.0, 0.0]), normal=jnp.array([1.0, 0.0, 0.0])
... )
>>> object_origins = jnp.array([[-1.0, -0.5, 0.5], [1.0, 0.0, 0.0]])
>>> object_vectors = jnp.array([
...     [[0.0, 1.0, 0.0], [0.0, 0.0, 0.0]],  # Edges only need one vector
...     [[0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],  # Planes need two vectors
... ])
>>> path = fermat_path_on_linear_objects(
...     from_vertex,
...     to_vertex,
...     object_origins,
...     object_vectors,
... )
>>> with reuse(backend="plotly") as fig:
...     wall.plot(color="blue")
...     mirror.plot(color="red")
...     draw_paths(
...         wall.triangle_edges[0, 1, ...],
...         line_color="yellow",
...         line_width=5,
...         name="Edge",
...     )
...
...     full_path = assemble_paths(
...         from_vertex,
...         path,
...         to_vertex,
...     )
...     draw_paths(full_path, marker={"color": "green"}, name="Final path")
...     markers = jnp.vstack((from_vertex, to_vertex))
...     draw_markers(
...         markers,
...         labels=["BS", "UE"],
...         marker={"color": "black"},
...         name="BS/UE",
...     )
...     fig.update_layout(scene_aspectmode="data")
>>> fig