.. _dev_compat_frameworks:
Compatibility with other Frameworks
===================================
Type conversions
----------------
Sionna RT is built on top of `Mitsuba 3 `_
which is based on the differentiable just-in-time compiler `Dr.Jit
`_.
For this reason, all tensors and arrays use Mitsuba data types, which themselves
are backend-dependent aliases of Dr.Jit data types. For example, if we use
Mitsuba on a CPU, the Mitsuba ``mi.Float`` data type is an alias for the Dr.Jit
data type ``drjit.llvm.ad.Float``. This can be seen from the code snippet below:
.. code-block:: python
import mitsuba as mi
import drjit as dr
# Set Mitsuba3 variant
# For details see https://mitsuba.readthedocs.io/en/stable/src/key_topics/variants.html#choosing-variants
mi.set_variant("llvm_ad_mono_polarized")
print(type(mi.Float([3])))
::
Dr.Jit arrays can exchange data with other array programming frameworks such as
`Numpy `_,
`Jax `_,
`TensorFlow `_, and `PyTorch `_.
Detailed information can be found in the `Dr.Jit Documentation `_.
Whenever possible, conversions between frameworks use a zero-copy strategy
relying on `DLPack `_. That means that no
additional memory is required and tensors are just exposed as a different type.
Conversion from Dr.Jit to other frameworks is as simple as calling the following methods on a Dr.Jit
array:
.. code-block:: python
# Note that the desired framework(s) need(s) to be installed for
# the following code to work.
x = mi.Float([1,2,3])
print(type(x.numpy()))
print(type(x.jax()))
print(type(x.tf()))
print(type(x.torch()))
::
The inverse direction is even simpler:
.. code-block:: python
import torch
a = torch.ones([3, 6], dtype=torch.float32)
a_dr = mi.TensorXf(a)
print(a_dr)
::
[[1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1]]
Gradients
---------
It is possible to exchange gradients between Dr.Jit and other frameworks with automatic gradient computation.
This can be achieved with the help of the `@dr.wrap `_ decorator.
The following code snippet shows how a function written in Dr.Jit can be exposed as if it was implemented in PyTorch:
.. code-block:: python
a = torch.ones([3, 6], dtype=torch.float32, requires_grad=True)
@dr.wrap(source="torch", target="drjit")
def fun(a):
return dr.sum(dr.abs(a)**2)
b = fun(a)
b.backward()
print(a.grad)
::
tensor([[2., 2., 2., 2., 2., 2.],
[2., 2., 2., 2., 2., 2.],
[2., 2., 2., 2., 2., 2.]])
Similarly, one can use a function written in PyTorch in the context of a larger
program implemented in Dr.Jit, as shown below:
.. code-block:: python
a = dr.ones(mi.TensorXf, [3, 6])
dr.enable_grad(a)
@dr.wrap(source="drjit", target="torch")
def fun(a):
return torch.sum(torch.abs(a)**2)
b = fun(a)
dr.backward(b)
print(a.grad)
::
[[2, 2, 2, 2, 2, 2],
[2, 2, 2, 2, 2, 2],
[2, 2, 2, 2, 2, 2]]
The `@dr.wrap` decorator supports also other frameworks such as Jax. Please
check the `documentation
`_ of the
latest version of Dr.Jit to see what is possible.
Training-Loop in PyTorch
------------------------
.. figure:: ../figures/dev_guide_torch_train.png
:align: center
:width: 70 %
Transmitter and receiver separated by a blocking wall
The following code snippet shows how one can implement a gradient-based
optimization loop in PyTorch affecting radio material properties in Sionna RT.
In this example, we have a transmitter and receiver that are separated by a
blocking wall. Only a single refracted path connects both. The goal is to
optimize the thickness and conductivity of the wall such that the received
signal strength is maximized. Obviously, this happens when the wall is removed,
i.e., it has a thickness of zero. For any nonzero thickness, the conductivity
should be made as small as possible to increase the energy of the refracted field.
.. code-block:: python
import torch
import numpy as np
import sionna.rt
from sionna.rt import load_scene, PlanarArray, Transmitter, Receiver, \
PathSolver, RadioMaterial, cpx_abs_square
# Load scene and place TX/RX
scene = load_scene(sionna.rt.scene.simple_reflector, merge_shapes=False)
scene.tx_array = PlanarArray(num_cols=1, num_rows=1,
pattern="iso", polarization="V")
scene.rx_array = scene.tx_array
scene.add(Transmitter("tx", position=[0,0,3]))
scene.add(Receiver("rx", position=[0,0,-3]))
# Create custom radio material and assign it to reflector
my_mat = RadioMaterial(name="my_mat",
conductivity=0.1,
thickness=0.1,
relative_permittivity=2.1)
scene.get("reflector").radio_material = my_mat
# Wrap path computation function within a PyTorch context
p_solver = PathSolver()
p_solver.loop_mode = "evaluated" # Needed for gradient compuation
@dr.wrap(source="torch", target="drjit")
def compute_paths(thickness, conductivity):
# Avoid negative values of thickness and conductivity
my_mat.thickness = dr.select(thickness.array<0, 0, thickness.array)
my_mat.conductivity = dr.select(conductivity.array<0, 0, conductivity.array)
paths = p_solver(scene, refraction=True)
gain = dr.sum(dr.sum(cpx_abs_square(paths.a)))
return gain
# PyTorch training loop maximizing the path gain
conductivity = torch.tensor(0.1, requires_grad=True)
thickness = torch.tensor(0.2, requires_grad=True)
optimizer = torch.optim.Adam([thickness, conductivity], lr=0.05)
num_steps = 10
for step in range(num_steps):
loss = -compute_paths(thickness, conductivity)
optimizer.zero_grad()
loss.backward()
optimizer.step()
if step in [0, num_steps-1]:
print("Step: ", step)
print("Path gain (dB): ", 10*np.log10(-loss.detach().numpy()))
print("Thickness: ", my_mat.thickness[0])
print("Conductivity: ", my_mat.conductivity[0])
print("------------------------------------\n")
::
Step: 0
Path gain (dB): -81.59713
Thickness: 0.15265434980392456
Conductivity: 0.05138068273663521
------------------------------------
Step: 9
Path gain (dB): -58.89217
Thickness: 0.0
Conductivity: 0.0
------------------------------------