anomaly-detection-material-parameters-calibration

Sionna param calibration (research proj)
git clone https://git.ea.contact/anomaly-detection-material-parameters-calibration
Log | Files | Refs | README

scene.py (85725B)


      1 #
      2 # SPDX-FileCopyrightText: Copyright (c) 2021-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
      3 # SPDX-License-Identifier: Apache-2.0
      4 #
      5 """
      6 A scene contains everything that is needed for rendering and radio propagation
      7 simulation. This includes the scene geometry, materials, transmitters,
      8 receivers, as well as cameras.
      9 """
     10 
     11 import os
     12 from importlib_resources import files
     13 
     14 import matplotlib
     15 import matplotlib.pyplot as plt
     16 import mitsuba as mi
     17 import tensorflow as tf
     18 import numpy as np
     19 
     20 from .antenna_array import AntennaArray
     21 from .camera import Camera
     22 from .radio_device import RadioDevice
     23 from sionna.constants import SPEED_OF_LIGHT, PI, BOLTZMANN_CONSTANT
     24 from .itu_materials import instantiate_itu_materials
     25 from .radio_material import RadioMaterial
     26 from .receiver import Receiver
     27 from .ris import RIS
     28 from .scene_object import SceneObject
     29 from .solver_paths import SolverPaths, PathsTmpData
     30 from .solver_cm import SolverCoverageMap
     31 from .transmitter import Transmitter
     32 from .previewer import InteractiveDisplay
     33 from .renderer import render, coverage_map_color_mapping
     34 from .utils import expand_to_rank
     35 from .paths import Paths
     36 from . import scenes
     37 
     38 
     39 class Scene:
     40     # pylint: disable=line-too-long
     41     r"""
     42     Scene()
     43 
     44     The scene contains everything that is needed for radio propagation simulation
     45     and rendering.
     46 
     47     A scene is a collection of multiple instances of :class:`~sionna.rt.SceneObject` which define
     48     the geometry and materials of the objects in the scene.
     49     The scene also includes transmitters (:class:`~sionna.rt.Transmitter`) and receivers (:class:`~sionna.rt.Receiver`)
     50     for which propagation :class:`~sionna.rt.Paths`, channel impulse responses (CIRs) or coverage maps (:class:`~sionna.rt.CoverageMap`) can be computed,
     51     as well as cameras (:class:`~sionna.rt.Camera`) for rendering.
     52 
     53     The only way to instantiate a scene is by calling :func:`~sionna.rt.load_scene()`.
     54     Note that only a single scene can be loaded at a time.
     55 
     56     Example scenes can be loaded as follows:
     57 
     58     .. code-block:: Python
     59 
     60         scene = load_scene(sionna.rt.scene.munich)
     61         scene.preview()
     62 
     63     .. figure:: ../figures/scene_preview.png
     64         :align: center
     65     """
     66 
     67     # Default frequency
     68     DEFAULT_FREQUENCY = 3.5e9 # Hz
     69 
     70     # Default frequency
     71     DEFAULT_BANDWIDTH = 1e6 # Hz
     72 
     73     # Default temperature
     74     DEFAULT_TEMPERATURE = 293 # K
     75 
     76     # This object is a singleton, as it is assumed that only one scene can be
     77     # loaded at a time.
     78     _instance = None
     79     def __new__(cls, *args, **kwargs): # pylint: disable=unused-argument
     80         if cls._instance is None:
     81             instance = object.__new__(cls)
     82 
     83             # Creates fields if required.
     84             # This is done only the first time the instance is created.
     85 
     86             # Transmitters
     87             instance._transmitters = {}
     88             # Receivers
     89             instance._receivers = {}
     90             # Reconfigurable intelligent surfaces
     91             instance._ris = {}
     92             # Reconfigurable intelligent surfaces
     93             instance._ris = {}
     94             # Cameras
     95             instance._cameras = {}
     96             # Transmitter antenna array
     97             instance._tx_array = None
     98             # Receiver antenna array
     99             instance._rx_array = None
    100             # Radio materials
    101             instance._radio_materials = {}
    102             # Scene objects
    103             instance._scene_objects = {}
    104             # By default, the antenna arrays is applied synthetically
    105             instance._synthetic_array = True
    106             # Holds a reference to the interactive preview widget
    107             instance._preview_widget = None
    108 
    109             # Set the unique instance of the scene
    110             cls._instance = instance
    111 
    112             # By default, no callable is used for radio materials
    113             cls._instance._radio_material_callable = None
    114 
    115             # By default, no callable is used for scattering patterns
    116             cls._instance._scattering_pattern_callable = None
    117 
    118         return cls._instance
    119 
    120     def __init__(self, env_filename = None, dtype = tf.complex64):
    121 
    122         # If a filename is provided, loads the scene from it.
    123         # The previous scene is overwritten.
    124         if env_filename:
    125 
    126             if dtype not in (tf.complex64, tf.complex128):
    127                 msg = "`dtype` must be tf.complex64 or tf.complex128`"
    128                 raise ValueError(msg)
    129             self._dtype = dtype
    130             self._rdtype = dtype.real_dtype
    131 
    132             # Clear it all
    133             self._clear()
    134 
    135             # Set the frequency to the default value
    136             self.frequency = Scene.DEFAULT_FREQUENCY
    137 
    138             # Set the bandwidth to the default value
    139             self.bandwidth = Scene.DEFAULT_BANDWIDTH
    140 
    141             # Set the temperature to the default value
    142             self.temperature = Scene.DEFAULT_TEMPERATURE
    143 
    144             # Populate with ITU materials
    145             instantiate_itu_materials(self._dtype)
    146 
    147             # Load the scene
    148             # Keep track of the Mitsuba scene
    149             if env_filename == "__empty__":
    150                 # Set an empty scene
    151                 self._scene = mi.load_dict({"type": "scene",
    152                                             "integrator": {
    153                                                 "type": "path",
    154                                             }})
    155             else:
    156                 self._scene = mi.load_file(env_filename, parallel=False)
    157             self._scene_params = mi.traverse(self._scene)
    158 
    159             # Load the cameras
    160             self._load_cameras()
    161 
    162             # Load the scene objects
    163             self._load_scene_objects()
    164 
    165             # By default, no callable is used for radio materials
    166             self.radio_material_callable = None
    167 
    168             # By default, no callable is used for scattering patterns
    169             self._scattering_pattern_callable = None
    170 
    171             # Instantiate the solver
    172             self._solver_paths = SolverPaths(self, dtype=dtype)
    173 
    174             # Solver for coverage map
    175             self._solver_cm = SolverCoverageMap(self, solver=self._solver_paths,
    176                                                 dtype=dtype)
    177 
    178     @property
    179     def cameras(self):
    180         """
    181         `dict` (read-only), { "name", :class:`~sionna.rt.Camera`} : Dictionary
    182                     of cameras in the scene
    183         """
    184         return dict(self._cameras)
    185 
    186     @property
    187     def frequency(self):
    188         """
    189         float : Get/set the carrier frequency [Hz]
    190 
    191             Setting the frequency updates the parameters of frequency-dependent
    192             radio materials. Defaults to 3.5e9.
    193         """
    194         return self._frequency
    195 
    196     @frequency.setter
    197     def frequency(self, f):
    198         if f <= 0.0:
    199             raise ValueError("Frequency must be positive")
    200         self._frequency = tf.cast(f, self._dtype.real_dtype)
    201         # Wavelength [m]
    202         self._wavelength = tf.cast(SPEED_OF_LIGHT/f,
    203                                     self._dtype.real_dtype)
    204 
    205         # Update radio materials
    206         for mat in self.radio_materials.values():
    207             mat.frequency_update()
    208 
    209     @property
    210     def temperature(self):
    211         """
    212         float : Get/set the environment temperature [K]. Used for the
    213         computation of :attr:`~sionna.rt.Scene.thermal_noise_power`.
    214         Defaults to 293.
    215         """
    216         return self._temperature
    217 
    218     @temperature.setter
    219     def temperature(self, v):
    220         if v<0:
    221             raise ValueError("temperature must be positive")
    222         self._temperature = tf.cast(v, self._rdtype)
    223 
    224     @property
    225     def bandwidth(self):
    226         """
    227         float : Get/set the transmission bandwidth [Hz]. Used for the
    228         computation of :attr:`~sionna.rt.Scene.thermal_noise_power`.
    229         Defaults to 1e6.
    230         """
    231         return self._bandwidth
    232 
    233     @bandwidth.setter
    234     def bandwidth(self, v):
    235         if v<0:
    236             raise ValueError("bandwidth must be positive")
    237         self._bandwidth = tf.cast(v, self._rdtype)
    238 
    239     @property
    240     def thermal_noise_power(self):
    241         """
    242         float : Get the thermal noise power [W]
    243         """
    244         return self.temperature * tf.cast(BOLTZMANN_CONSTANT, self._rdtype) \
    245                * self.bandwidth
    246 
    247     @property
    248     def wavelength(self):
    249         """
    250         float (read-only) :  Get the wavelength [m]
    251         """
    252         return self._wavelength
    253 
    254     @property
    255     def wavenumber(self):
    256         r"""
    257         float (read-only) :  Get the wavenumber :math:`k=2\pi/\lambda` [m^-1]
    258         """
    259         return tf.cast(2*PI, self._dtype.real_dtype)/self._wavelength
    260 
    261     @property
    262     def synthetic_array(self):
    263         """
    264         bool : Get/set if the antenna arrays are applied synthetically.
    265             Defaults to `True`.
    266         """
    267         return self._synthetic_array
    268 
    269     @synthetic_array.setter
    270     def synthetic_array(self, value):
    271         if not isinstance(value, bool):
    272             raise TypeError("'synthetic_array' must be boolean")
    273         self._synthetic_array = value
    274 
    275     @property
    276     def dtype(self):
    277         r"""
    278         `tf.complex64 | tf.complex128` : Datatype used in tensors
    279         """
    280         return self._dtype
    281 
    282     @property
    283     def transmitters(self):
    284         # pylint: disable=line-too-long
    285         """
    286         `dict` (read-only), { "name", :class:`~sionna.rt.Transmitter`} : Dictionary
    287             of transmitters in the scene
    288         """
    289         return dict(self._transmitters)
    290 
    291     @property
    292     def receivers(self):
    293         """
    294         `dict` (read-only), { "name", :class:`~sionna.rt.Receiver`} : Dictionary
    295              of receivers in the scene
    296         """
    297         return dict(self._receivers)
    298 
    299     @property
    300     def ris(self):
    301         """
    302         `dict` (read-only), { "name", :class:`~sionna.rt.RIS`} : Dictionary
    303              of reconfigurable intelligent surfaces (RIS) in the scene
    304         """
    305         return dict(self._ris)
    306 
    307     @property
    308     def radio_materials(self):
    309         # pylint: disable=line-too-long
    310         """
    311         `dict` (read-only), { "name", :class:`~sionna.rt.RadioMaterial`} : Dictionary
    312             of radio materials
    313         """
    314         return dict(self._radio_materials)
    315 
    316     @property
    317     def objects(self):
    318         # pylint: disable=line-too-long
    319         """
    320         `dict` (read-only), { "name", :class:`~sionna.rt.SceneObject`} : Dictionary
    321             of scene objects
    322         """
    323         return dict(self._scene_objects)
    324 
    325     @property
    326     def tx_array(self):
    327         """
    328         :class:`~sionna.rt.AntennaArray` : Get/set the antenna array used by
    329             all transmitters in the scene. Defaults to `None`.
    330         """
    331         return self._tx_array
    332 
    333     @tx_array.setter
    334     def tx_array(self, array):
    335         if not isinstance(array, AntennaArray):
    336             raise TypeError("`array` must be an instance of ``AntennaArray``")
    337         self._tx_array = array
    338 
    339     @property
    340     def rx_array(self):
    341         """
    342         :class:`~sionna.rt.AntennaArray` : Get/set the antenna array used by
    343             all receivers in the scene. Defaults to `None`.
    344         """
    345         return self._rx_array
    346 
    347     @rx_array.setter
    348     def rx_array(self, array):
    349         if not isinstance(array, AntennaArray):
    350             raise TypeError("`array` must be an instance of ``AntennaArray``")
    351         self._rx_array = array
    352 
    353     @property
    354     def size(self):
    355         """
    356         [3], tf.float : Get the size of the scene, i.e., the size of the
    357         axis-aligned minimum bounding box for the scene
    358         """
    359         size = tf.cast(self._scene.bbox().max - self._scene.bbox().min,
    360                        self._rdtype)
    361         return size
    362 
    363     @property
    364     def center(self):
    365         """
    366         [3], tf.float : Get the center of the scene
    367         """
    368         size = tf.cast((self._scene.bbox().max + self._scene.bbox().min)*0.5,
    369                        self._rdtype)
    370         return size
    371 
    372     def get(self, name):
    373         # pylint: disable=line-too-long
    374         """
    375         Returns a scene object, transmitter, receiver, camera, or radio material
    376 
    377         Input
    378         -----
    379         name : str
    380             Name of the item to retrieve
    381 
    382         Output
    383         ------
    384         item : :class:`~sionna.rt.SceneObject` | :class:`~sionna.rt.RadioMaterial` | :class:`~sionna.rt.Transmitter` | :class:`~sionna.rt.Receiver` | :class:`~sionna.rt.RIS` | :class:`~sionna.rt.Camera` | `None`
    385             Retrieved item. Returns `None` if no corresponding item was found in the scene.
    386         """
    387         if name in self._transmitters:
    388             return self._transmitters[name]
    389         if name in self._receivers:
    390             return self._receivers[name]
    391         if name in self._ris:
    392             return self._ris[name]
    393         if name in self._ris:
    394             return self._ris[name]
    395         if name in self._radio_materials:
    396             return self._radio_materials[name]
    397         if name in self._scene_objects:
    398             return self._scene_objects[name]
    399         if name in self._cameras:
    400             return self._cameras[name]
    401         return None
    402 
    403     def add(self, item):
    404         # pylint: disable=line-too-long
    405         # pylint: disable=line-too-long
    406         """
    407         Adds a transmitter, receiver, RIS, radio material, or camera to the scene.
    408 
    409         If a different item with the same name as ``item`` is already part of the scene,
    410         an error is raised.
    411 
    412         Input
    413         ------
    414         item : :class:`~sionna.rt.Transmitter` | :class:`~sionna.rt.Receiver` | :class:`~sionna.rt.RIS` | :class:`~sionna.rt.RadioMaterial` | :class:`~sionna.rt.Camera`
    415             Item to add to the scene
    416         """
    417         if ( (not isinstance(item, Camera))
    418          and (not isinstance(item, RadioDevice))
    419          and (not isinstance(item, RadioMaterial)) ):
    420             err_msg = "The input must be a Transmitter, Receiver, RIS, Camera, or"\
    421                       " RadioMaterial"
    422             raise ValueError(err_msg)
    423 
    424         name = item.name
    425         s_item = self.get(name)
    426         if s_item is not None:
    427             if  s_item is not item:
    428                 # In the case of RadioMaterial, the current item with same
    429                 # name could just be a placeholder
    430                 if (isinstance(s_item, RadioMaterial)
    431                     and isinstance(item, RadioMaterial)
    432                     and s_item.is_placeholder):
    433                     s_item.assign(item)
    434                     s_item.is_placeholder = False
    435                 else:
    436                     msg = f"Name '{name}' is already used by another item of"\
    437                            " the scene"
    438                     raise ValueError(msg)
    439             else:
    440                 # This item was already added.
    441                 return
    442         if isinstance(item, Transmitter):
    443             self._transmitters[name] = item
    444             item.scene = self
    445         elif isinstance(item, Receiver):
    446             self._receivers[name] = item
    447             item.scene = self
    448         elif isinstance(item, RIS):
    449             self._ris[name] = item
    450             # Manually assign object_id to each RIS
    451             if len(self.objects)>0:
    452                 max_id = max(obj.object_id for obj in self.objects.values())
    453             else:
    454                 max_id=0
    455             max_id += len(self._ris)
    456             item.object_id = max_id
    457             # Set scene propety and radio material
    458             item.scene = self
    459             item.radio_material = "itu_metal"
    460         elif isinstance(item, RadioMaterial):
    461             self._radio_materials[name] = item
    462             item.frequency_update()
    463         elif isinstance(item, Camera):
    464             self._cameras[name] = item
    465             item.scene = self
    466 
    467     def remove(self, name):
    468         # pylint: disable=line-too-long
    469         """
    470         Removes a transmitter, receiver, RIS, camera, or radio material from the
    471         scene.
    472 
    473         In the case of a radio material, it must not be used by any object of
    474         the scene.
    475 
    476         Input
    477         -----
    478         name : str
    479             Name of the item to remove
    480         """
    481         if not isinstance(name, str):
    482             raise ValueError("The input should be a string")
    483         item = self.get(name)
    484 
    485         if item is None:
    486             pass
    487 
    488         elif isinstance(item, Transmitter):
    489             del self._transmitters[name]
    490 
    491         elif isinstance(item, Receiver):
    492             del self._receivers[name]
    493 
    494         elif isinstance(item, RIS):
    495             del self._ris[name]
    496 
    497         elif isinstance(item, RIS):
    498             del self._ris[name]
    499 
    500         elif isinstance(item, Camera):
    501             del self._cameras[name]
    502 
    503         elif isinstance(item, RadioMaterial):
    504             if item.is_used:
    505                 msg = f"The radio material '{name}' is used by at least one"\
    506                         " object"
    507                 raise ValueError(msg)
    508             del self._radio_materials[name]
    509 
    510         else:
    511             msg = "Only Transmitters, Receivers, RIS, Cameras, or RadioMaterials"\
    512                   " can be removed"
    513             raise TypeError(msg)
    514 
    515 
    516     def trace_paths(self, max_depth=3, method="fibonacci", num_samples=int(1e6),
    517                     los=True, reflection=True, diffraction=False,
    518                     scattering=False, ris=True, scat_keep_prob=0.001,
    519                     edge_diffraction=False, check_scene=True):
    520         # pylint: disable=line-too-long
    521         r"""
    522         Computes the trajectories of the paths by shooting rays.
    523 
    524         The EM fields corresponding to the traced paths are not computed.
    525         They can be computed using :meth:`~sionna.rt.Scene.compute_fields()`:
    526 
    527         .. code-block:: Python
    528 
    529             traced_paths = scene.trace_paths()
    530             paths = scene.compute_fields(*traced_paths)
    531 
    532         Path tracing is independent of the radio materials, antenna patterns,
    533         and radio device orientations.
    534         Therefore, a set of traced paths could be reused for different values
    535         of these quantities, e.g., to calibrate the ray tracer.
    536         This can enable significant resource savings as path tracing is
    537         typically significantly more resource-intensive than field computation.
    538 
    539         Note that :meth:`~sionna.rt.Scene.compute_paths()` does both path tracing and
    540         field computation.
    541 
    542         Input
    543         ------
    544         max_depth : int
    545             Maximum depth (i.e., number of interaction with objects in the scene)
    546             allowed for tracing the paths.
    547             Defaults to 3.
    548 
    549         method : str ("exhaustive"|"fibonacci")
    550             Method to be used to list candidate paths.
    551             The "exhaustive" method tests all possible combination of primitives as
    552             paths. This method is not compatible with scattering.
    553             The "fibonacci" method uses a shoot-and-bounce approach to find
    554             candidate chains of primitives. Initial ray directions are arranged
    555             in a Fibonacci lattice on the unit sphere. This method can be
    556             applied to very large scenes. However, there is no guarantee that
    557             all possible paths are found.
    558             Defaults to "fibonacci".
    559 
    560         num_samples: int
    561             Number of random rays to trace in order to generate candidates.
    562             A large sample count may exhaust GPU memory.
    563             Defaults to 1e6. Only needed if ``method`` is "fibonacci".
    564 
    565         los : bool
    566             If set to `True`, then the LoS paths are computed.
    567             Defaults to `True`.
    568 
    569         reflection : bool
    570             If set to `True`, then the reflected paths are computed.
    571             Defaults to `True`.
    572 
    573         diffraction : bool
    574             If set to `True`, then the diffracted paths are computed.
    575             Defaults to `False`.
    576 
    577         scattering : bool
    578             If set to `True`, then the scattered paths are computed.
    579             Only works with the Fibonacci method.
    580             Defaults to `False`.
    581 
    582         ris : bool
    583             If set to `True`, then the paths involving RIS are computed.
    584             Defaults to `True`.
    585 
    586         scat_keep_prob : float
    587             Probability with which to keep scattered paths.
    588             This is helpful to reduce the number of scattered paths computed,
    589             which might be prohibitively high in some setup.
    590             Must be in the range (0,1).
    591             Defaults to 0.001.
    592 
    593         edge_diffraction : bool
    594             If set to `False`, only diffraction on wedges, i.e., edges that
    595             connect two primitives, is considered.
    596             Defaults to `False`.
    597 
    598         check_scene : bool
    599             If set to `True`, checks that the scene is well configured before
    600             computing the paths. This can add a significant overhead.
    601             Defaults to `True`.
    602 
    603         Output
    604         -------
    605         spec_paths : :class:`~sionna.rt.Paths`
    606             Computed specular paths
    607 
    608         diff_paths : :class:`~sionna.rt.Paths`
    609             Computed diffracted paths
    610 
    611         scat_paths : :class:`~sionna.rt.Paths`
    612             Computed scattered paths
    613 
    614         ris_paths : :class:`~sionna.rt.Paths`
    615             Computed paths involving RIS
    616 
    617         spec_paths_tmp : :class:`~sionna.rt.PathsTmpData`
    618             Additional data required to compute the EM fields of the specular
    619             paths
    620 
    621         diff_paths_tmp : :class:`~sionna.rt.PathsTmpData`
    622             Additional data required to compute the EM fields of the diffracted
    623             paths
    624 
    625         scat_paths_tmp : :class:`~sionna.rt.PathsTmpData`
    626             Additional data required to compute the EM fields of the scattered
    627             paths
    628 
    629         ris_paths_tmp : :class:`~sionna.rt.PathsTmpData`
    630             Additional data required to compute the EM fields of the paths
    631             involving RIS
    632 
    633         """
    634 
    635         if scat_keep_prob < 0. or scat_keep_prob > 1.:
    636             msg = "The parameter `scat_keep_prob` must be in the range (0,1)"
    637             raise ValueError(msg)
    638 
    639         # Check that all is set to compute paths
    640         if check_scene:
    641             self._check_scene(False)
    642 
    643         # Trace the paths
    644         paths = self._solver_paths.trace_paths(max_depth,
    645                                                method=method,
    646                                                num_samples=num_samples,
    647                                                los=los, reflection=reflection,
    648                                                diffraction=diffraction,
    649                                                scattering=scattering,
    650                                                ris=ris,
    651                                                scat_keep_prob=scat_keep_prob,
    652                                                edge_diffraction=edge_diffraction)
    653 
    654         return paths
    655 
    656     def compute_fields(self, spec_paths, diff_paths, scat_paths, ris_paths,
    657                        spec_paths_tmp, diff_paths_tmp, scat_paths_tmp,
    658                        ris_paths_tmp, check_scene=True, scat_random_phases=True,
    659                        testing=False):
    660         r"""compute_fields(self, spec_paths, diff_paths, scat_paths, spec_paths_tmp, diff_paths_tmp, scat_paths_tmp, check_scene=True, scat_random_phases=True)
    661         Computes the EM fields corresponding to traced paths.
    662 
    663         Paths can be traced using :meth:`~sionna.rt.Scene.trace_paths()`.
    664         This method can then be used to finalize the paths calculation by
    665         computing the corresponding fields:
    666 
    667         .. code-block:: Python
    668 
    669             traced_paths = scene.trace_paths()
    670             paths = scene.compute_fields(*traced_paths)
    671 
    672         Paths tracing is independent from the radio materials, antenna patterns,
    673         and radio devices orientations.
    674         Therefore, a set of traced paths could be reused for different values
    675         of these quantities, e.g., to calibrate the ray tracer.
    676         This can enable significant resource savings as paths tracing is
    677         typically significantly more resource-intensive than field computation.
    678 
    679         Note that :meth:`~sionna.rt.Scene.compute_paths()` does both tracing and
    680         field computation.
    681 
    682         Input
    683         ------
    684         spec_paths : :class:`~sionna.rt.Paths`
    685             Specular paths
    686 
    687         diff_paths : :class:`~sionna.rt.Paths`
    688             Diffracted paths
    689 
    690         scat_paths : :class:`~sionna.rt.Paths`
    691             Scattered paths
    692 
    693         ris_paths : :class:`~sionna.rt.Paths`
    694             Computed paths involving RIS
    695 
    696         ris_paths : :class:`~sionna.rt.Paths`
    697             Computed paths involving RIS
    698 
    699         spec_paths_tmp : :class:`~sionna.rt.PathsTmpData`
    700             Additional data required to compute the EM fields of the specular
    701             paths
    702 
    703         diff_paths_tmp : :class:`~sionna.rt.PathsTmpData`
    704             Additional data required to compute the EM fields of the diffracted
    705             paths
    706 
    707         scat_paths_tmp : :class:`~sionna.rt.PathsTmpData`
    708             Additional data required to compute the EM fields of the scattered
    709             paths
    710 
    711         ris_paths_tmp : :class:`~sionna.rt.PathsTmpData`
    712             Additional data required to compute the EM fields of the paths
    713             involving RIS
    714 
    715         ris_paths_tmp : :class:`~sionna.rt.PathsTmpData`
    716             Additional data required to compute the EM fields of the paths
    717             involving RIS
    718 
    719         check_scene : bool
    720             If set to `True`, checks that the scene is well configured before
    721             computing the paths. This can add a significant overhead.
    722             Defaults to `True`.
    723 
    724         scat_random_phases : bool
    725             If set to `True` and if scattering is enabled, random uniform phase
    726             shifts are added to the scattered paths.
    727             Defaults to `True`.
    728 
    729         Output
    730         -------
    731         paths : :class:`~sionna.rt.Paths`
    732             Computed paths
    733         """
    734 
    735         # Check that all is set to compute paths
    736         if check_scene:
    737             self._check_scene(False)
    738 
    739         # Compute the fields and merge the paths
    740         output = self._solver_paths.compute_fields(spec_paths, diff_paths,
    741             scat_paths, ris_paths, spec_paths_tmp, diff_paths_tmp,
    742             scat_paths_tmp, ris_paths_tmp,
    743             scat_random_phases, testing)
    744         sources, targets, paths_as_dict = output[:3]
    745         paths = Paths(sources, targets, self)
    746         paths.from_dict(paths_as_dict)
    747 
    748         # If the hidden input flag testing is True, additional data
    749         # is returned which is required for some unit tests
    750         if testing:
    751             spec_tmp_as_dict, diff_tmp_as_dict, scat_tmp_as_dict = output[3:]
    752             spec_tmp = PathsTmpData(sources, targets, self._dtype)
    753             spec_tmp.from_dict(spec_tmp_as_dict)
    754             diff_tmp = PathsTmpData(sources, targets, self._dtype)
    755             diff_tmp.from_dict(diff_tmp_as_dict)
    756             scat_tmp = PathsTmpData(sources, targets, self._dtype)
    757             scat_tmp.from_dict(scat_tmp_as_dict)
    758             paths.spec_tmp = spec_tmp
    759             paths.diff_tmp = diff_tmp
    760             paths.scat_tmp = scat_tmp
    761 
    762         # Finalize paths computation
    763         paths.finalize()
    764 
    765         return paths
    766 
    767     def compute_paths(self, max_depth=3, method="fibonacci",
    768                       num_samples=int(1e6), los=True, reflection=True,
    769                       diffraction=False, scattering=False, ris=True,
    770                       scat_keep_prob=0.001, edge_diffraction=False,
    771                       check_scene=True, scat_random_phases=True,
    772                       testing=False):
    773         # pylint: disable=line-too-long
    774         r"""
    775         Computes propagation paths.
    776 
    777         This function computes propagation paths between the antennas of
    778         all transmitters and receivers in the current scene.
    779         For each propagation path :math:`i`, the corresponding channel coefficient
    780         :math:`a_i` and delay :math:`\tau_i`, as well as the
    781         angles of departure :math:`(\theta_{\text{T},i}, \varphi_{\text{T},i})`
    782         and arrival :math:`(\theta_{\text{R},i}, \varphi_{\text{R},i})` are returned.
    783         For more detail, see :eq:`H_final`.
    784         Different propagation phenomena, such as line-of-sight, reflection, diffraction,
    785         and diffuse scattering can be individually enabled/disabled.
    786 
    787         If the scene is configured to use synthetic arrays
    788         (:attr:`~sionna.rt.Scene.synthetic_array` is `True`), transmitters and receivers
    789         are modelled as if they had a single antenna located at their
    790         :attr:`~sionna.rt.Transmitter.position`. The channel responses for each
    791         individual antenna of the arrays are then computed "synthetically" by applying
    792         appropriate phase shifts. This reduces the complexity significantly
    793         for large arrays. Time evolution of the channel coefficients can be simulated with
    794         the help of the function :meth:`~sionna.rt.Paths.apply_doppler` of the returned
    795         :class:`~sionna.rt.Paths` object.
    796 
    797         The path computation consists of two main steps as shown in the below figure.
    798 
    799         .. figure:: ../figures/compute_paths.svg
    800             :align: center
    801 
    802         For a configured :class:`~sionna.rt.Scene`, the function first traces geometric propagation paths
    803         using :meth:`~sionna.rt.Scene.trace_paths`. This step is independent of the
    804         :class:`~sionna.rt.RadioMaterial` of the scene objects as well as the transmitters' and receivers'
    805         antenna :attr:`~sionna.rt.Antenna.patterns` and  :attr:`~sionna.rt.Transmitter.orientation`,
    806         but depends on the selected propagation
    807         phenomena, such as reflection, scattering, and diffraction. The traced paths
    808         are then converted to EM fields by the function :meth:`~sionna.rt.Scene.compute_fields`.
    809         The resulting :class:`~sionna.rt.Paths` object can be used to compute channel
    810         impulse responses via :meth:`~sionna.rt.Paths.cir`. The advantage of separating path tracing
    811         and field computation is that one can study the impact of different radio materials
    812         by executing :meth:`~sionna.rt.Scene.compute_fields` multiple times without
    813         re-tracing the propagation paths. This can for example speed-up the calibration of scene parameters
    814         by several orders of magnitude.
    815 
    816         Example
    817         -------
    818         .. code-block:: Python
    819 
    820             import sionna
    821             from sionna.rt import load_scene, Camera, Transmitter, Receiver, PlanarArray
    822 
    823             # Load example scene
    824             scene = load_scene(sionna.rt.scene.munich)
    825 
    826             # Configure antenna array for all transmitters
    827             scene.tx_array = PlanarArray(num_rows=8,
    828                                       num_cols=2,
    829                                       vertical_spacing=0.7,
    830                                       horizontal_spacing=0.5,
    831                                       pattern="tr38901",
    832                                       polarization="VH")
    833 
    834             # Configure antenna array for all receivers
    835             scene.rx_array = PlanarArray(num_rows=1,
    836                                       num_cols=1,
    837                                       vertical_spacing=0.5,
    838                                       horizontal_spacing=0.5,
    839                                       pattern="dipole",
    840                                       polarization="cross")
    841 
    842             # Create transmitter
    843             tx = Transmitter(name="tx",
    844                           position=[8.5,21,27],
    845                           orientation=[0,0,0])
    846             scene.add(tx)
    847 
    848             # Create a receiver
    849             rx = Receiver(name="rx",
    850                        position=[45,90,1.5],
    851                        orientation=[0,0,0])
    852             scene.add(rx)
    853 
    854             # TX points towards RX
    855             tx.look_at(rx)
    856 
    857             # Compute paths
    858             paths = scene.compute_paths()
    859 
    860             # Open preview showing paths
    861             scene.preview(paths=paths, resolution=[1000,600])
    862 
    863         .. figure:: ../figures/paths_preview.png
    864             :align: center
    865 
    866         Input
    867         ------
    868         max_depth : int
    869             Maximum depth (i.e., number of bounces) allowed for tracing the
    870             paths. Defaults to 3.
    871 
    872         method : str ("exhaustive"|"fibonacci")
    873             Ray tracing method to be used.
    874             The "exhaustive" method tests all possible combinations of primitives.
    875             This method is not compatible with scattering.
    876             The "fibonacci" method uses a shoot-and-bounce approach to find
    877             candidate chains of primitives. Initial ray directions are chosen
    878             according to a Fibonacci lattice on the unit sphere. This method can be
    879             applied to very large scenes. However, there is no guarantee that
    880             all possible paths are found.
    881             Defaults to "fibonacci".
    882 
    883         num_samples : int
    884             Number of rays to trace in order to generate candidates with
    885             the "fibonacci" method.
    886             This number is split equally among the different transmitters
    887             (when using synthetic arrays) or transmit antennas (when not using
    888             synthetic arrays).
    889             This parameter is ignored when using the exhaustive method.
    890             Tracing more rays can lead to better precision
    891             at the cost of increased memory requirements.
    892             Defaults to 1e6.
    893 
    894         los : bool
    895             If set to `True`, then the LoS paths are computed.
    896             Defaults to `True`.
    897 
    898         reflection : bool
    899             If set to `True`, then the reflected paths are computed.
    900             Defaults to `True`.
    901 
    902         diffraction : bool
    903             If set to `True`, then the diffracted paths are computed.
    904             Defaults to `False`.
    905 
    906         scattering : bool
    907             If set to `True`, then the scattered paths are computed.
    908             If set to `True`, then the scattered paths are computed.
    909             Only works with the Fibonacci method.
    910             Defaults to `False`.
    911 
    912         ris : bool
    913             If set to `True`, then paths involving RIS are computed.
    914             Defaults to `True`.
    915 
    916         scat_keep_prob : float
    917             Probability with which a scattered path is kept.
    918             This is helpful to reduce the number of computed scattered
    919             paths, which might be prohibitively high in some scenes.
    920             Must be in the range (0,1). Defaults to 0.001.
    921 
    922         edge_diffraction : bool
    923             If set to `False`, only diffraction on wedges, i.e., edges that
    924             connect two primitives, is considered.
    925             Defaults to `False`.
    926 
    927         check_scene : bool
    928             If set to `True`, checks that the scene is well configured before
    929             computing the paths. This can add a significant overhead.
    930             Defaults to `True`.
    931 
    932         scat_random_phases : bool
    933             If set to `True` and if scattering is enabled, random uniform phase
    934             shifts are added to the scattered paths.
    935             Defaults to `True`.
    936 
    937         testing : bool
    938             If set to `True`, then additional data is returned for testing.
    939             Defaults to `False`.
    940 
    941         Output
    942         ------
    943         :paths : :class:`~sionna.rt.Paths`
    944             Simulated paths
    945         """
    946 
    947         # Trace the paths
    948         traced_paths = self.trace_paths(max_depth, method, num_samples, los,
    949             reflection, diffraction, scattering, ris, scat_keep_prob,
    950             edge_diffraction, check_scene)
    951 
    952         # Compute the fields and merge the paths
    953         # Check scene is not done twice
    954         paths = self.compute_fields(*traced_paths, False, scat_random_phases,
    955                                     testing)
    956 
    957         return paths
    958 
    959     def coverage_map(self,
    960                      rx_orientation=(0.,0.,0.),
    961                      max_depth=3,
    962                      cm_center=None,
    963                      cm_orientation=None,
    964                      cm_size=None,
    965                      cm_cell_size=(10.,10.),
    966                      combining_vec=None,
    967                      precoding_vec=None,
    968                      num_samples=int(2e6),
    969                      los=True,
    970                      reflection=True,
    971                      diffraction=False,
    972                      scattering=False,
    973                      ris=True,
    974                      edge_diffraction=False,
    975                      check_scene=True,
    976                      num_runs=1):
    977         # pylint: disable=line-too-long
    978         r"""
    979         This function computes a coverage map for every transmitter in the scene.
    980 
    981         For a given transmitter, a coverage map is a rectangular surface with
    982         arbitrary orientation subdivded
    983         into rectangular cells of size :math:`\lvert C \rvert = \texttt{cm_cell_size[0]} \times  \texttt{cm_cell_size[1]}`.
    984         The parameter ``cm_cell_size`` therefore controls the granularity of the map.
    985         The coverage map associates with every cell :math:`(i,j)` the quantity
    986 
    987         .. math::
    988             :label: cm_def
    989 
    990             g_{i,j} = \frac{1}{\lvert C \rvert} \int_{C_{i,j}} \lvert h(s) \rvert^2 ds
    991 
    992         where :math:`\lvert h(s) \rvert^2` is the squared amplitude
    993         of the path coefficients :math:`a_i` at position :math:`s=(x,y)`,
    994         the integral is over the cell :math:`C_{i,j}`, and
    995         :math:`ds` is the infinitesimal small surface element
    996         :math:`ds=dx \cdot dy`.
    997         The dimension indexed by :math:`i` (:math:`j`) corresponds to the :math:`y\, (x)`-axis of the
    998         coverage map in its local coordinate system. The quantity
    999         :math:`g_{i,j}` can be seen as the average :attr:`~sionna.rt.CoverageMap.path_gain` across a cell.
   1000 
   1001         The path gain can be transformed into the received signal strength (:attr:`~sionna.rt.CoverageMap.rss`)
   1002         by multiplying it with the transmit :attr:`~sionna.rt.Transmitter.power`:
   1003 
   1004         .. math::
   1005 
   1006             \mathrm{RSS}_{i,j} = P_{tx} g_{i,j}.
   1007 
   1008         If a scene has multiple transmitters, the
   1009         signal-to-interference-plus-noise ratio
   1010         (:attr:`~sionna.rt.Transmitter.sinr`) for transmitter :math:`k` is then
   1011         defined as
   1012 
   1013         .. math::
   1014 
   1015             \mathrm{SINR}^k_{i,j}=\frac{\mathrm{RSS}^k_{i,j}}{N_0+\sum_{k'\ne k} \mathrm{RSS}^{k'}_{i,j}}
   1016 
   1017         where :math:`N_0` [W] is the :attr:`~sionna.rt.Scene.thermal_noise_power`, computed as:
   1018 
   1019         .. math::
   1020 
   1021             N_0 = B \times T \times k
   1022 
   1023         where :math:`B` [Hz] is the transmission :attr:`~sionna.rt.Scene.bandwidth`,
   1024         :math:`T` [K] is the :attr:`~sionna.rt.Scene.temperature`, and
   1025         :math:`k=1.380649\times 10^{-23}` [J/K] is the Boltzmann constant.
   1026 
   1027         For specularly and diffusely reflected paths, :eq:`cm_def` can be rewritten as an integral over the directions
   1028         of departure of the rays from the transmitter, by substituting :math:`s`
   1029         with the corresponding direction :math:`\omega`:
   1030 
   1031         .. math::
   1032             g_{i,j} = \frac{1}{\lvert C \rvert} \int_{\Omega} \lvert h\left(s(\omega) \right) \rvert^2 \frac{r(\omega)^2}{\lvert \cos{\alpha(\omega)} \rvert} \mathbb{1}_{\left\{ s(\omega) \in C_{i,j} \right\}} d\omega
   1033 
   1034         where the integration is over the unit sphere :math:`\Omega`, :math:`r(\omega)` is the length of
   1035         the path with direction of departure :math:`\omega`, :math:`s(\omega)` is the point
   1036         where the path with direction of departure :math:`\omega` intersects the coverage map,
   1037         :math:`\alpha(\omega)` is the angle between the coverage map normal and the direction of arrival
   1038         of the path with direction of departure :math:`\omega`,
   1039         and :math:`\mathbb{1}_{\left\{ s(\omega) \in C_{i,j} \right\}}` is the function that takes as value
   1040         one if :math:`s(\omega) \in C_{i,j}` and zero otherwise.
   1041         Note that :math:`ds = \frac{r(\omega)^2 d\omega}{\lvert \cos{\alpha(\omega)} \rvert}`.
   1042 
   1043         The previous integral is approximated through Monte Carlo sampling by shooting :math:`N` rays
   1044         with directions :math:`\omega_n` arranged as a Fibonacci lattice on the unit sphere around the transmitter,
   1045         and bouncing the rays on the intersected objects until the maximum depth (``max_depth``) is reached or
   1046         the ray bounces out of the scene.
   1047         At every intersection with an object of the scene, a new ray is shot from the intersection which corresponds to either
   1048         specular reflection or diffuse scattering, following a Bernoulli distribution with parameter the
   1049         squared scattering coefficient.
   1050         When diffuse scattering is selected, the direction of the scattered ray is uniformly sampled on the half-sphere.
   1051         The resulting Monte Carlo estimate is:
   1052 
   1053         .. math::
   1054             :label: cm_mc_ref
   1055 
   1056             \hat{g}_{i,j}^{\text{(ref)}} = \frac{4\pi}{N\lvert C \rvert} \sum_{n=1}^N \lvert h\left(s(\omega_n)\right)  \rvert^2 \frac{r(\omega_n)^2}{\lvert \cos{\alpha(\omega_n)} \rvert} \mathbb{1}_{\left\{ s(\omega_n) \in C_{i,j} \right\}}.
   1057 
   1058         For the diffracted paths, :eq:`cm_def` can be rewritten for any wedge
   1059         with length :math:`L` and opening angle :math:`\Phi` as an integral over the wedge and its opening angle,
   1060         by substituting :math:`s` with the position on the wedge :math:`\ell \in [1,L]` and the angle :math:`\phi \in [0, \Phi]`:
   1061 
   1062         .. math::
   1063             g_{i,j} = \frac{1}{\lvert C \rvert} \int_{\ell} \int_{\phi} \lvert h\left(s(\ell,\phi) \right) \rvert^2 \mathbb{1}_{\left\{ s(\ell,\phi) \in C_{i,j} \right\}} \left\lVert \frac{\partial r}{\partial \ell} \times \frac{\partial r}{\partial \phi} \right\rVert d\ell d\phi
   1064 
   1065         where the integral is over the wedge length :math:`L` and opening angle :math:`\Phi`, and
   1066         :math:`r\left( \ell, \phi \right)` is the reparametrization with respected to :math:`(\ell, \phi)` of the
   1067         intersection between the diffraction cone at :math:`\ell` and the rectangle defining the coverage map (see, e.g., [SurfaceIntegral]_).
   1068         The previous integral is approximated through Monte Carlo sampling by shooting :math:`N'` rays from equally spaced
   1069         locations :math:`\ell_n` along the wedge with directions :math:`\phi_n` sampled uniformly from :math:`(0, \Phi)`:
   1070 
   1071         .. math::
   1072             :label: cm_mc_diff
   1073 
   1074             \hat{g}_{i,j}^{\text{(diff)}} = \frac{L\Phi}{N'\lvert C \rvert} \sum_{n=1}^{N'} \lvert h\left(s(\ell_n,\phi_n)\right) \rvert^2 \mathbb{1}_{\left\{ s(\ell_n,\phi_n) \in C_{i,j} \right\}} \left\lVert \left(\frac{\partial r}{\partial \ell}\right)_n \times \left(\frac{\partial r}{\partial \phi}\right)_n \right\rVert.
   1075 
   1076         The output of this function is therefore a real-valued matrix of size ``[num_cells_y, num_cells_x]``,
   1077         for every transmitter, with elements equal to the sum of the contributions of the reflected and scattered paths
   1078         :eq:`cm_mc_ref` and diffracted paths :eq:`cm_mc_diff` for all the wedges, and where
   1079 
   1080         .. math::
   1081             \texttt{num_cells_x} = \bigg\lceil\frac{\texttt{cm_size[0]}}{\texttt{cm_cell_size[0]}} \bigg\rceil\\
   1082             \texttt{num_cells_y} = \bigg\lceil \frac{\texttt{cm_size[1]}}{\texttt{cm_cell_size[1]}} \bigg\rceil.
   1083 
   1084         The surface defining the coverage map is a rectangle centered at
   1085         ``cm_center``, with orientation ``cm_orientation``, and with size
   1086         ``cm_size``. An orientation of (0,0,0) corresponds to
   1087         a coverage map parallel to the XY plane, with surface normal pointing towards
   1088         the :math:`+z` axis. By default, the coverage map
   1089         is parallel to the XY plane, covers all of the scene, and has
   1090         an elevation of :math:`z = 1.5\text{m}`.
   1091         The receiver is assumed to use the antenna array
   1092         ``scene.rx_array``. If transmitter and/or receiver have multiple antennas, transmit precoding
   1093         and receive combining are applied which are defined by ``precoding_vec`` and
   1094         ``combining_vec``, respectively.
   1095 
   1096         The :math:`(i,j)` indices are omitted in the following for clarity.
   1097         For reflection and scattering, paths are generated by shooting ``num_samples`` rays from the
   1098         transmitters with directions arranged in a Fibonacci lattice on the unit
   1099         sphere and by simulating their propagation for up to ``max_depth`` interactions with
   1100         scene objects.
   1101         If ``max_depth`` is set to 0 and if ``los`` is set to `True`,
   1102         only the line-of-sight path is considered.
   1103         For diffraction, paths are generated by shooting ``num_samples`` rays from equally
   1104         spaced locations along the wedges in line-of-sight with the transmitter, with
   1105         directions uniformly sampled on the diffraction cone.
   1106 
   1107         For every ray :math:`n` intersecting the coverage map cell :math:`(i,j)`, the
   1108         channel coefficients, :math:`a_n`, and the angles of departure (AoDs)
   1109         :math:`(\theta_{\text{T},n}, \varphi_{\text{T},n})`
   1110         and arrival (AoAs) :math:`(\theta_{\text{R},n}, \varphi_{\text{R},n})`
   1111         are computed. See the `Primer on Electromagnetics <../em_primer.html>`_ for more details.
   1112 
   1113         A "synthetic" array is simulated by adding additional phase shifts that depend on the
   1114         antenna position relative to the position of the transmitter (receiver) as well as the AoDs (AoAs).
   1115         For the :math:`k^\text{th}` transmit antenna and :math:`\ell^\text{th}` receive antenna, let
   1116         us denote by :math:`\mathbf{d}_{\text{T},k}` and :math:`\mathbf{d}_{\text{R},\ell}` the relative positions (with respect to
   1117         the positions of the transmitter/receiver) of the pair of antennas
   1118         for which the channel impulse response shall be computed. These can be accessed through the antenna array's property
   1119         :attr:`~sionna.rt.AntennaArray.positions`. Using a plane-wave assumption, the resulting phase shifts
   1120         from these displacements can be computed as
   1121 
   1122         .. math::
   1123 
   1124             p_{\text{T}, n,k} &= \frac{2\pi}{\lambda}\hat{\mathbf{r}}(\theta_{\text{T},n}, \varphi_{\text{T},n})^\mathsf{T} \mathbf{d}_{\text{T},k}\\
   1125             p_{\text{R}, n,\ell} &= \frac{2\pi}{\lambda}\hat{\mathbf{r}}(\theta_{\text{R},n}, \varphi_{\text{R},n})^\mathsf{T} \mathbf{d}_{\text{R},\ell}.
   1126 
   1127         The final expression for the path coefficient is
   1128 
   1129         .. math::
   1130 
   1131             h_{n,k,\ell} =  a_n e^{j(p_{\text{T}, i,k} + p_{\text{R}, i,\ell})}
   1132 
   1133         for every transmit antenna :math:`k` and receive antenna :math:`\ell`.
   1134         These coefficients form the complex-valued channel matrix, :math:`\mathbf{H}_n`,
   1135         of size :math:`\texttt{num_rx_ant} \times \texttt{num_tx_ant}`.
   1136 
   1137         Finally, the coefficient of the equivalent SISO channel is
   1138 
   1139         .. math::
   1140             h_n =  \mathbf{c}^{\mathsf{H}} \mathbf{H}_n \mathbf{p}
   1141 
   1142         where :math:`\mathbf{c}` and :math:`\mathbf{p}` are the combining and
   1143         precoding vectors (``combining_vec`` and ``precoding_vec``),
   1144         respectively.
   1145 
   1146         Example
   1147         -------
   1148         .. code-block:: Python
   1149 
   1150             import sionna
   1151             from sionna.rt import load_scene, PlanarArray, Transmitter, Receiver
   1152             scene = load_scene(sionna.rt.scene.munich)
   1153 
   1154             # Configure antenna array for all transmitters
   1155             scene.tx_array = PlanarArray(num_rows=8,
   1156                                     num_cols=2,
   1157                                     vertical_spacing=0.7,
   1158                                     horizontal_spacing=0.5,
   1159                                     pattern="tr38901",
   1160                                     polarization="VH")
   1161 
   1162             # Configure antenna array for all receivers
   1163             scene.rx_array = PlanarArray(num_rows=1,
   1164                                     num_cols=1,
   1165                                     vertical_spacing=0.5,
   1166                                     horizontal_spacing=0.5,
   1167                                     pattern="dipole",
   1168                                     polarization="cross")
   1169             # Add a transmitters
   1170             tx = Transmitter(name="tx",
   1171                         position=[8.5,21,30],
   1172                         orientation=[0,0,0])
   1173             scene.add(tx)
   1174             tx.look_at([40,80,1.5])
   1175 
   1176             # Compute coverage map
   1177             cm = scene.coverage_map(cm_cell_size=[1.,1.],
   1178                                 num_samples=int(10e6))
   1179 
   1180             # Visualize coverage in preview
   1181             scene.preview(coverage_map=cm,
   1182                         resolution=[1000, 600])
   1183 
   1184         .. figure:: ../figures/coverage_map_preview.png
   1185             :align: center
   1186 
   1187         Input
   1188         ------
   1189         rx_orientation : [3], float
   1190             Orientation of the receiver :math:`(\alpha, \beta, \gamma)`
   1191             specified through three angles corresponding to a 3D rotation
   1192             as defined in :eq:`rotation`. Defaults to :math:`(0,0,0)`.
   1193 
   1194         max_depth : int
   1195             Maximum depth (i.e., number of bounces) allowed for tracing the
   1196             paths. Defaults to 3.
   1197 
   1198         cm_center : [3], float | `None`
   1199             Center of the coverage map :math:`(x,y,z)` as three-dimensional
   1200             vector. If set to `None`, the coverage map is centered on the
   1201             center of the scene, except for the elevation :math:`z` that is set
   1202             to 1.5m. Otherwise, ``cm_orientation`` and ``cm_scale`` must also
   1203             not be `None`. Defaults to `None`.
   1204 
   1205         cm_orientation : [3], float | `None`
   1206             Orientation of the coverage map :math:`(\alpha, \beta, \gamma)`
   1207             specified through three angles corresponding to a 3D rotation
   1208             as defined in :eq:`rotation`.
   1209             An orientation of :math:`(0,0,0)` or `None` corresponds to a
   1210             coverage map that is parallel to the XY plane.
   1211             If not set to `None`, then ``cm_center`` and ``cm_scale`` must also
   1212             not be `None`.
   1213             Defaults to `None`.
   1214 
   1215         cm_size : [2], float | `None`
   1216             Size of the coverage map [m].
   1217             If set to `None`, then the size of the coverage map is set such that
   1218             it covers the entire scene.
   1219             Otherwise, ``cm_center`` and ``cm_orientation`` must also not be
   1220             `None`. Defaults to `None`.
   1221 
   1222         cm_cell_size : [2], float
   1223             Size of a cell of the coverage map [m].
   1224             Defaults to :math:`(10,10)`.
   1225 
   1226         combining_vec : [num_rx_ant], complex | None
   1227             Combining vector.
   1228             If set to `None`, then no combining is applied, and
   1229             the energy received by all antennas is summed.
   1230 
   1231         precoding_vec : [num_tx_ant] | [num_tx, num_tx_ant], complex | None
   1232             Precoding vector.
   1233             If set to `None`, then defaults to
   1234             :math:`\frac{1}{\sqrt{\text{num_tx_ant}}} [1,\dots,1]^{\mathsf{T}}`.
   1235 
   1236         num_samples : int
   1237             Number of random rays to trace.
   1238             For the reflected paths, this number is split equally over the different transmitters.
   1239             For the diffracted paths, it is split over the wedges in line-of-sight with the
   1240             transmitters such that the number of rays allocated
   1241             to a wedge is proportional to its length.
   1242             Defaults to 2e6.
   1243 
   1244         los : bool
   1245             If set to `True`, then the LoS paths are computed.
   1246             Defaults to `True`.
   1247 
   1248         reflection : bool
   1249             If set to `True`, then the reflected paths are computed.
   1250             Defaults to `True`.
   1251 
   1252         diffraction : bool
   1253             If set to `True`, then the diffracted paths are computed.
   1254             Defaults to `False`.
   1255 
   1256         scattering : bool
   1257             If set to `True`, then the scattered paths are computed.
   1258             Defaults to `False`.
   1259 
   1260         ris : bool
   1261             If set to `True`, then paths involving RIS are computed.
   1262             Defaults to `True`.
   1263 
   1264         edge_diffraction : bool
   1265             If set to `False`, only diffraction on wedges, i.e., edges that
   1266             connect two primitives, is considered.
   1267             Defaults to `False`.
   1268 
   1269         check_scene : bool
   1270             If set to `True`, checks that the scene is well configured before
   1271             computing the coverage map. This can add a significant overhead.
   1272             Defaults to `True`.
   1273 
   1274         num_runs : int, >= 1
   1275             Number of times the coverage map solver is executed. The returned
   1276             coverage map is the average over all runs. If set to a value greater
   1277             than one, a random rotation is applied to the Fibonacci lattice at
   1278             each run.
   1279             Using mutiple runs can reduce noise in the coverage map without
   1280             increasing ``num_samples`` and the related memory footprint.
   1281             Defaults to 1.
   1282 
   1283         Output
   1284         ------
   1285         :cm : :class:`~sionna.rt.CoverageMap`
   1286             Coverage map
   1287         """
   1288 
   1289         # Check that all is set to compute the coverage map
   1290         if check_scene:
   1291             self._check_scene(True)
   1292 
   1293         # Check the properties of the rectangle defining the coverage map
   1294         if ((cm_center is None)
   1295             and (cm_size is None)
   1296             and (cm_orientation is None)):
   1297             # Default value for center: Center of the scene
   1298             # Default value for the scale: Just enough to cover all the scene
   1299             # with axis-aligned edges of the rectangle
   1300             # [min_x, min_y, min_z]
   1301             scene_min = self._scene.bbox().min
   1302             scene_min = tf.cast(scene_min, self._rdtype)
   1303             # In case of empty scene, bbox min is -inf
   1304             scene_min = tf.where(tf.math.is_inf(scene_min),
   1305                                  -tf.ones_like(scene_min),
   1306                                  scene_min)
   1307             # [max_x, max_y, max_z]
   1308             scene_max = self._scene.bbox().max
   1309             scene_max = tf.cast(scene_max, self._rdtype)
   1310             # In case of empty scene, bbox min is inf
   1311             scene_max = tf.where(tf.math.is_inf(scene_max),
   1312                                  tf.ones_like(scene_max),
   1313                                  scene_max)
   1314             cm_center = tf.cast([(scene_min[0] + scene_max[0])*0.5,
   1315                                  (scene_min[1] + scene_max[1])*0.5,
   1316                                  1.5], dtype=self._rdtype)
   1317             cm_size = tf.cast([(scene_max[0] - scene_min[0]),
   1318                                (scene_max[1] - scene_min[1])],
   1319                                 dtype=self._rdtype)
   1320             # Set the orientation to default value
   1321             cm_orientation = tf.zeros([3], dtype=self._rdtype)
   1322         elif ((cm_center is None)
   1323               or (cm_size is None)
   1324               or (cm_orientation is None)):
   1325             raise ValueError("If one of `cm_center`, `cm_orientation`,"\
   1326                              " or `cm_size` is not None, then all of them"\
   1327                              " must not be None")
   1328         else:
   1329             cm_center = tf.cast(cm_center, self._rdtype)
   1330             cm_orientation = tf.cast(cm_orientation, self._rdtype)
   1331             cm_size = tf.cast(cm_size, self._rdtype)
   1332 
   1333         # Check and initialize the combining and precoding vector
   1334         if combining_vec is not None:
   1335             combining_vec = tf.cast(combining_vec, self._dtype)
   1336         num_tx = len(self.transmitters)
   1337         if precoding_vec is None:
   1338             precoding_vec = tf.ones([num_tx, self.tx_array.num_ant],
   1339                                     self._dtype)
   1340             precoding_vec /= tf.sqrt(tf.cast(self.tx_array.num_ant,
   1341                                              self._dtype))
   1342         else:
   1343             precoding_vec = tf.cast(precoding_vec, self._dtype)
   1344             precoding_vec = expand_to_rank(precoding_vec, 2, 0)
   1345             if precoding_vec.shape[0] == 1:
   1346                 precoding_vec = tf.tile(precoding_vec, [num_tx, 1])
   1347 
   1348         # [3]
   1349         rx_orientation = tf.cast(rx_orientation, self._rdtype)
   1350 
   1351         # Compute the coverage map using the solver
   1352         # [num_sources, num_cells_x, num_cells_y]
   1353         output = self._solver_cm(max_depth=max_depth,
   1354                                  rx_orientation=rx_orientation,
   1355                                  cm_center=cm_center,
   1356                                  cm_orientation=cm_orientation,
   1357                                  cm_size=cm_size,
   1358                                  cm_cell_size=cm_cell_size,
   1359                                  combining_vec=combining_vec,
   1360                                  precoding_vec=precoding_vec,
   1361                                  num_samples=num_samples,
   1362                                  los=los,
   1363                                  reflection=reflection,
   1364                                  diffraction=diffraction,
   1365                                  scattering=scattering,
   1366                                  ris=ris,
   1367                                  edge_diffraction=edge_diffraction,
   1368                                  num_runs=num_runs)
   1369 
   1370         return output
   1371 
   1372     def preview(self, paths=None, show_paths=True, show_devices=True,
   1373                 show_orientations=False,
   1374                 coverage_map=None, cm_tx=None, cm_db_scale=True,
   1375                 cm_vmin=None, cm_vmax=None, cm_metric="path_gain",
   1376                 resolution=(655, 500), fov=45, background='#ffffff',
   1377                 clip_at=None, clip_plane_orientation=(0, 0, -1)):
   1378         # pylint: disable=line-too-long
   1379         r"""In an interactive notebook environment, opens an interactive 3D viewer of the scene.
   1380 
   1381         The returned value of this method must be the last line of
   1382         the cell so that it is displayed. For example:
   1383 
   1384         .. code-block:: Python
   1385 
   1386             fig = scene.preview()
   1387             # ...
   1388             fig
   1389 
   1390         Or simply:
   1391 
   1392         .. code-block:: Python
   1393 
   1394             scene.preview()
   1395 
   1396         Default color coding:
   1397 
   1398         * Green: Receiver
   1399         * Blue: Transmitter
   1400         * Red: Reconfigurable Intelligent Surface (RIS)
   1401 
   1402         Controls:
   1403 
   1404         * Mouse left: Rotate
   1405         * Scroll wheel: Zoom
   1406         * Mouse right: Move
   1407 
   1408         Input
   1409         -----
   1410         paths : :class:`~sionna.rt.Paths` | `None`
   1411             Simulated paths generated by
   1412             :meth:`~sionna.rt.Scene.compute_paths()` or `None`.
   1413             If `None`, only the scene is rendered.
   1414             Defaults to `None`.
   1415 
   1416         show_paths : bool
   1417             If `paths` is not `None`, shows the paths.
   1418             Defaults to `True`.
   1419 
   1420         show_devices : bool
   1421             If set to `True`, shows the radio devices.
   1422             Defaults to `True`.
   1423 
   1424         show_orientations : bool
   1425             If `show_devices` is `True`, shows the radio devices orientations.
   1426             Defaults to `False`.
   1427 
   1428         coverage_map : :class:`~sionna.rt.CoverageMap` | `None`
   1429             An optional coverage map to overlay in the scene for visualization.
   1430             Defaults to `None`.
   1431 
   1432         cm_tx : int | str | None
   1433             When `coverage_map` is specified, controls which of the transmitters
   1434             to display the coverage map for. Either the transmitter's name
   1435             or index can be given. If `None`, the maximum metric over all
   1436             transmitters is shown.
   1437             Defaults to `None`.
   1438 
   1439         cm_db_scale: bool
   1440             Use logarithmic scale for coverage map visualization, i.e. the
   1441             coverage values are mapped with:
   1442             :math:`y = 10 \cdot \log_{10}(x)`.
   1443             Defaults to `True`.
   1444 
   1445         cm_vmin, cm_vmax: floot | None
   1446             For coverage map visualization, defines the range of path gains that
   1447             the colormap covers.
   1448             These parameters should be provided in dB if ``cm_db_scale`` is
   1449             set to `True`, or in linear scale otherwise.
   1450             If set to None, then covers the complete range.
   1451             Defaults to `None`.
   1452 
   1453         cm_metric : str, one of ["path_gain", "rss", "sinr"]
   1454             Metric of the coverage map to be displayed.
   1455             Defaults to `path_gain`.
   1456 
   1457         resolution : [2], int
   1458             Size of the viewer figure.
   1459             Defaults to `[655, 500]`.
   1460 
   1461         fov : float
   1462             Field of view, in degrees.
   1463             Defaults to 45°.
   1464 
   1465         background : str
   1466             Background color in hex format prefixed by '#'.
   1467             Defaults to '#ffffff' (white).
   1468 
   1469         clip_at : float
   1470             If not `None`, the scene preview will be clipped (cut) by a plane
   1471             with normal orientation ``clip_plane_orientation`` and offset ``clip_at``.
   1472             That means that everything *behind* the plane becomes invisible.
   1473             This allows visualizing the interior of meshes, such as buildings.
   1474             Defaults to `None`.
   1475 
   1476         clip_plane_orientation : tuple[float, float, float]
   1477             Normal vector of the clipping plane.
   1478             Defaults to (0,0,-1).
   1479         """
   1480         if (self._preview_widget is not None) and (resolution is not None):
   1481             assert isinstance(resolution, (tuple, list)) and len(resolution) == 2
   1482             if tuple(resolution) != self._preview_widget.resolution():
   1483                 # User requested a different rendering resolution, create
   1484                 # a new viewer from scratch to match it.
   1485                 self._preview_widget = None
   1486 
   1487         # Cache the render widget so that we don't need to re-create it
   1488         # every time
   1489         fig = self._preview_widget
   1490         needs_reset = fig is not None
   1491         if needs_reset:
   1492             fig.reset()
   1493         else:
   1494             fig = InteractiveDisplay(scene=self,
   1495                                      resolution=resolution,
   1496                                      fov=fov,
   1497                                      background=background)
   1498             self._preview_widget = fig
   1499 
   1500         # Show paths and devices, if required
   1501         if show_paths and (paths is not None):
   1502             fig.plot_paths(paths)
   1503         if show_devices:
   1504             fig.plot_radio_devices(show_orientations=show_orientations)
   1505             fig.plot_ris()
   1506         if coverage_map is not None:
   1507             fig.plot_coverage_map(
   1508                 coverage_map, tx=cm_tx, db_scale=cm_db_scale,
   1509                 vmin=cm_vmin, vmax=cm_vmax, metric=cm_metric)
   1510 
   1511         # Clipping
   1512         fig.set_clipping_plane(offset=clip_at, orientation=clip_plane_orientation)
   1513 
   1514         # Update the camera state
   1515         if not needs_reset:
   1516             fig.center_view()
   1517 
   1518         return fig
   1519 
   1520     def render(self, camera, paths=None, show_paths=True, show_devices=True,
   1521                coverage_map=None, cm_tx=None, cm_db_scale=True,
   1522                cm_vmin=None, cm_vmax=None, cm_metric="path_gain", cm_show_color_bar=True,
   1523                num_samples=512, resolution=(655, 500), fov=45):
   1524         # pylint: disable=line-too-long
   1525         r"""Renders the scene from the viewpoint of a camera or the interactive viewer
   1526 
   1527         Input
   1528         ------
   1529         camera : str | :class:`~sionna.rt.Camera`
   1530             The name or instance of a :class:`~sionna.rt.Camera`.
   1531             If an interactive viewer was opened with
   1532             :meth:`~sionna.rt.Scene.preview()`, set to `"preview"` to use its
   1533             viewpoint.
   1534 
   1535         paths : :class:`~sionna.rt.Paths` | `None`
   1536             Simulated paths generated by
   1537             :meth:`~sionna.rt.Scene.compute_paths()` or `None`.
   1538             If `None`, only the scene is rendered.
   1539             Defaults to `None`.
   1540 
   1541         show_paths : bool
   1542             If `paths` is not `None`, shows the paths.
   1543             Defaults to `True`.
   1544 
   1545         show_devices : bool
   1546             If `paths` is not `None`, shows the radio devices.
   1547             Defaults to `True`.
   1548 
   1549         coverage_map : :class:`~sionna.rt.CoverageMap` | `None`
   1550             An optional coverage map to overlay in the scene for visualization.
   1551             Defaults to `None`.
   1552 
   1553         cm_tx : int | str | None
   1554             When `coverage_map` is specified, controls which of the transmitters
   1555             to display the coverage map for. Either the transmitter's name
   1556             or index can be given. If `None`, the maximum metric over all
   1557             transmitters is shown.
   1558             Defaults to `None`.
   1559 
   1560         cm_db_scale: bool
   1561             Use logarithmic scale for coverage map visualization, i.e. the
   1562             coverage values are mapped with:
   1563             :math:`y = 10 \cdot \log_{10}(x)`.
   1564             Defaults to `True`.
   1565 
   1566         cm_vmin, cm_vmax: float | None
   1567             For coverage map visualization, defines the range of path gains that
   1568             the colormap covers.
   1569             These parameters should be provided in dB if ``cm_db_scale`` is
   1570             set to `True`, or in linear scale otherwise.
   1571             If set to None, then covers the complete range.
   1572             Defaults to `None`.
   1573 
   1574         cm_metric : str, one of ["path_gain", "rss", "sinr"]
   1575             Metric of the coverage map to be displayed.
   1576             Defaults to `path_gain`.
   1577 
   1578         cm_show_color_bar: bool
   1579             For coverage map visualization, show the color bar describing the
   1580             color mapping used next to the rendering.
   1581             Defaults to `True`.
   1582 
   1583         num_samples : int
   1584             Number of rays thrown per pixel.
   1585             Defaults to 512.
   1586 
   1587         resolution : [2], int
   1588             Size of the rendered figure.
   1589             Defaults to `[655, 500]`.
   1590 
   1591         fov : float
   1592             Field of view, in degrees.
   1593             Defaults to 45°.
   1594 
   1595         Output
   1596         -------
   1597         : :class:`~matplotlib.pyplot.Figure`
   1598             Rendered image
   1599         """
   1600 
   1601         image = render(scene=self,
   1602                        camera=camera,
   1603                        paths=paths,
   1604                        show_paths=show_paths,
   1605                        show_devices=show_devices,
   1606                        coverage_map=coverage_map,
   1607                        cm_tx=cm_tx,
   1608                        cm_db_scale=cm_db_scale,
   1609                        cm_vmin=cm_vmin,
   1610                        cm_vmax=cm_vmax,
   1611                        cm_metric=cm_metric,
   1612                        num_samples=num_samples,
   1613                        resolution=resolution,
   1614                        fov=fov)
   1615 
   1616         to_show = image.convert(component_format=mi.Struct.Type.UInt8,
   1617                                 srgb_gamma=True)
   1618 
   1619         show_color_bar = (coverage_map is not None) and cm_show_color_bar
   1620 
   1621         if show_color_bar:
   1622             aspect = image.width()*1.06 / image.height()
   1623             fig, ax = plt.subplots(1, 2,
   1624                                    gridspec_kw={'width_ratios': [0.97, 0.03]},
   1625                                    figsize=(aspect * 6, 6))
   1626             im_ax = ax[0]
   1627         else:
   1628             aspect = image.width() / image.height()
   1629             fig, ax = plt.subplots(1, 1, figsize=(aspect * 6, 6))
   1630             im_ax = ax
   1631 
   1632         im_ax.imshow(to_show)
   1633 
   1634         if show_color_bar:
   1635             cm = getattr(coverage_map, cm_metric).numpy()
   1636             if cm_tx is None:
   1637                 cm = np.max(cm, axis=0)
   1638             else:
   1639                 cm = cm[cm_tx]
   1640                 # Ensure that dBm is correctly computed for RSS
   1641             if cm_metric=="rss" and cm_db_scale:
   1642                 cm *= 1000
   1643             _, normalizer, color_map = coverage_map_color_mapping(
   1644                 cm, db_scale=cm_db_scale,
   1645                 vmin=cm_vmin, vmax=cm_vmax)
   1646             mappable = matplotlib.cm.ScalarMappable(
   1647                 norm=normalizer, cmap=color_map)
   1648 
   1649             cax = ax[1]
   1650             if cm_metric=="rss" and cm_db_scale:
   1651                 cax.set_title("dBm")
   1652             else:
   1653                 cax.set_title('dB')
   1654             fig.colorbar(mappable, cax=cax)
   1655 
   1656         # Remove axes and margins
   1657         im_ax.axis('off')
   1658         fig.tight_layout()
   1659         return fig
   1660 
   1661     def render_to_file(self, camera, filename, paths=None, show_paths=True, show_devices=True,
   1662                        coverage_map=None, cm_tx=None, cm_db_scale=True,
   1663                        cm_vmin=None, cm_vmax=None, cm_metric="path_gain",
   1664                        num_samples=512, resolution=(655, 500), fov=45):
   1665         # pylint: disable=line-too-long
   1666         r"""Renders the scene from the viewpoint of a camera or the interactive
   1667         viewer, and saves the resulting image
   1668 
   1669         Input
   1670         ------
   1671         camera : str | :class:`~sionna.rt.Camera`
   1672             The name or instance of a :class:`~sionna.rt.Camera`.
   1673             If an interactive viewer was opened with
   1674             :meth:`~sionna.rt.Scene.preview()`, set to `"preview"` to use its
   1675             viewpoint.
   1676 
   1677         filename : str
   1678             Filename for saving the rendered image, e.g., "my_scene.png"
   1679 
   1680         paths : :class:`~sionna.rt.Paths` | `None`
   1681             Simulated paths generated by
   1682             :meth:`~sionna.rt.Scene.compute_paths()` or `None`.
   1683             If `None`, only the scene is rendered.
   1684             Defaults to `None`.
   1685 
   1686         show_paths : bool
   1687             If `paths` is not `None`, shows the paths.
   1688             Defaults to `True`.
   1689 
   1690         show_devices : bool
   1691             If `paths` is not `None`, shows the radio devices.
   1692             Defaults to `True`.
   1693 
   1694         coverage_map : :class:`~sionna.rt.CoverageMap` | `None`
   1695             An optional coverage map to overlay in the scene for visualization.
   1696             Defaults to `None`.
   1697 
   1698         cm_tx : int | str | None
   1699             When `coverage_map` is specified, controls which of the transmitters
   1700             to display the coverage map for. Either the transmitter's name
   1701             or index can be given. If `None`, the maximum metric over all
   1702             transmitters is shown.
   1703             Defaults to `None`.
   1704 
   1705         cm_db_scale: bool
   1706             Use logarithmic scale for coverage map visualization, i.e. the
   1707             coverage values are mapped with:
   1708             :math:`y = 10 \cdot \log_{10}(x)`.
   1709             Defaults to `True`.
   1710 
   1711         cm_vmin, cm_vmax: float | None
   1712             For coverage map visualization, defines the range of path gains that
   1713             the colormap covers.
   1714             These parameters should be provided in dB if ``cm_db_scale`` is
   1715             set to `True`, or in linear scale otherwise.
   1716             If set to None, then covers the complete range.
   1717             Defaults to `None`.
   1718 
   1719         cm_metric : str, one of ["path_gain", "rss", "sinr"]
   1720             Metric of the coverage map to be displayed.
   1721             Defaults to `path_gain`.
   1722 
   1723         num_samples : int
   1724             Number of rays thrown per pixel.
   1725             Defaults to 512.
   1726 
   1727         resolution : [2], int
   1728             Size of the rendered figure.
   1729             Defaults to `[655, 500]`.
   1730 
   1731         fov : float
   1732             Field of view, in degrees.
   1733             Defaults to 45°.
   1734 
   1735         """
   1736         image = render(scene=self,
   1737                        camera=camera,
   1738                        paths=paths,
   1739                        show_paths=show_paths,
   1740                        show_devices=show_devices,
   1741                        coverage_map=coverage_map,
   1742                        cm_tx=cm_tx,
   1743                        cm_db_scale=cm_db_scale,
   1744                        cm_vmin=cm_vmin,
   1745                        cm_vmax=cm_vmax,
   1746                        cm_metric=cm_metric,
   1747                        num_samples=num_samples,
   1748                        resolution=resolution,
   1749                        fov=fov)
   1750 
   1751         ext = os.path.splitext(filename)[1].lower()
   1752         if ext in ('.jpg', '.jpeg', '.ppm',):
   1753             image = image.convert(component_format=mi.Struct.Type.UInt8,
   1754                                   pixel_format=mi.Bitmap.PixelFormat.RGB,
   1755                                   srgb_gamma=True)
   1756         elif ext in ('.png', '.tga' '.bmp'):
   1757             image = image.convert(component_format=mi.Struct.Type.UInt8,
   1758                                   srgb_gamma=True)
   1759         image.write(filename)
   1760 
   1761     @property
   1762     def radio_material_callable(self):
   1763         # pylint: disable=line-too-long
   1764         r"""
   1765         Get/set a callable that computes the radio material properties at the
   1766         points of intersection between the rays and the scene objects.
   1767 
   1768         If set, then the :class:`~sionna.rt.RadioMaterial` of the objects are
   1769         not used and the callable is invoked instead to obtain the
   1770         electromagnetic properties required to simulate the propagation of radio
   1771         waves.
   1772 
   1773         If not set, i.e., `None` (default), then the
   1774         :class:`~sionna.rt.RadioMaterial` of objects are used to simulate the
   1775         propagation of radio waves in the scene.
   1776 
   1777         This callable is invoked on batches of intersection points.
   1778         It takes as input the following tensors:
   1779 
   1780         * ``object_id`` (`[batch_dims]`, `int`) : Integers uniquely identifying the intersected objects
   1781         * ``points`` (`[batch_dims, 3]`, `float`) : Positions of the intersection points
   1782 
   1783         The callable must output a tuple/list of the following tensors:
   1784 
   1785         * ``complex_relative_permittivity`` (`[batch_dims]`, `complex`) : Complex relative permittivities :math:`\eta` :eq:`eta`
   1786         * ``scattering_coefficient`` (`[batch_dims]`, `float`) : Scattering coefficients :math:`S\in[0,1]` :eq:`scattering_coefficient`
   1787         * ``xpd_coefficient`` (`[batch_dims]`, `float`) : Cross-polarization discrimination coefficients :math:`K_x\in[0,1]` :eq:`xpd`. Only relevant for the scattered field.
   1788 
   1789         **Note:** The number of batch dimensions is not necessarily equal to one.
   1790         """
   1791         return self._radio_material_callable
   1792 
   1793     @radio_material_callable.setter
   1794     def radio_material_callable(self, rm_callable):
   1795         self._radio_material_callable = rm_callable
   1796 
   1797     @property
   1798     def scattering_pattern_callable(self):
   1799         # pylint: disable=line-too-long
   1800         r"""
   1801         Get/set a callable that computes the scattering pattern at the
   1802         points of intersection between the rays and the scene objects.
   1803 
   1804         If set, then the :attr:`~sionna.rt.RadioMaterial.scattering_pattern` of
   1805         the radio materials of the objects are not used and the callable is invoked
   1806         instead to evaluate the scattering pattern required to simulate the
   1807         propagation of diffusely reflected radio waves.
   1808 
   1809         If not set, i.e., `None` (default), then the
   1810         :attr:`~sionna.rt.RadioMaterial.scattering_pattern` of the objects'
   1811         radio materials are used to simulate the propagation of diffusely
   1812         reflected radio waves in the scene.
   1813 
   1814         This callable is invoked on batches of intersection points.
   1815         It takes as input the following tensors:
   1816 
   1817         * ``object_id`` (`[batch_dims]`, `int`) : Integers uniquely identifying the intersected objects
   1818         * ``points`` (`[batch_dims, 3]`, `float`) : Positions of the intersection points
   1819         * ``k_i`` (`[batch_dims, 3]`, `float`) : Unitary vector corresponding to the direction of incidence in the scene's global coordinate system
   1820         * ``k_s`` (`[batch_dims, 3]`, `float`) : Unitary vector corresponding to the direction of the diffuse reflection in the scene's global coordinate system
   1821         * ``n`` (`[batch_dims, 3]`, `float`) : Unitary vector corresponding to the normal to the surface at the intersection point
   1822 
   1823         The callable must output the following tensor:
   1824 
   1825         * ``f_s`` (`[batch_dims]`, `float`) : The scattering pattern evaluated for the previous inputs
   1826 
   1827         **Note:** The number of batch dimensions is not necessarily equal to one.
   1828         """
   1829         return self._scattering_pattern_callable
   1830 
   1831     @scattering_pattern_callable.setter
   1832     def scattering_pattern_callable(self, sp_callable):
   1833         self._scattering_pattern_callable = sp_callable
   1834 
   1835     @property
   1836     def mi_shapes(self):
   1837         # pylint: disable=line-too-long
   1838         """
   1839         `list` (read-only), [:class:`mi.Shape`] : List of Mitsuba shapes of the scene. The index of the shapes in this list defines the shapes IDs.
   1840         """
   1841         return self._mi_shapes
   1842 
   1843     ##############################################
   1844     # Internal methods.
   1845     # Should not be appear in the user
   1846     # documentation
   1847     ##############################################
   1848 
   1849     @property
   1850     def mi_scene(self):
   1851         """
   1852         :class:`~mitsuba.Scene` : Get the Mitsuba scene
   1853         """
   1854         return self._scene
   1855 
   1856     @property
   1857     def mi_scene_params(self):
   1858         """
   1859         :class:`~mitsuba.SceneParameters` : Get the Mitsuba scene parameters
   1860         """
   1861         return self._scene_params
   1862 
   1863     @property
   1864     def solver_paths(self):
   1865         """
   1866         :class:`~sionna.rt.SolverPaths` : Get the paths solver
   1867         """
   1868         return self._solver_paths
   1869 
   1870     @property
   1871     def solver_cm(self):
   1872         """
   1873         :class:`~sionna.rt.SolverCoverageMap` : Get the coverage map solver
   1874         """
   1875         return self._solver_cm
   1876 
   1877     @property
   1878     def preview_widget(self):
   1879         """
   1880         :class:`~sionna.rt.InteractiveDisplay` : Get the preview widget
   1881         """
   1882         return self._preview_widget
   1883 
   1884     @property
   1885     def mi2sionna_shift_obj_id(self):
   1886         """
   1887         int : Value to substract to the Mitsuba IDs (shape pointers) to obtain
   1888         Sionna IDs
   1889         """
   1890         return self._mi2sionna_shift_obj_id
   1891 
   1892     def scene_geometry_updated(self):
   1893         """
   1894         Callback to trigger when the scene geometry is updated
   1895         """
   1896         # Update the scene geometry in the preview
   1897         if self._preview_widget:
   1898             self._preview_widget.redraw_scene_geometry()
   1899 
   1900     def _clear(self):
   1901         r"""
   1902         Clear everything.
   1903         Should be called when a new scene is loaded.
   1904         """
   1905 
   1906         self._transmitters.clear()
   1907         self._receivers.clear()
   1908         self._ris.clear()
   1909         self._ris.clear()
   1910         self._cameras.clear()
   1911         self._radio_materials.clear()
   1912         self._scene_objects.clear()
   1913         self._tx_array = None
   1914         self._rx_array = None
   1915         self._preview_widget = None
   1916 
   1917     def _check_scene(self, coverage_map):
   1918         r"""
   1919         Check that all is set for paths or coverage map computation.
   1920         If not, raises an exception with the appropriate error message.
   1921 
   1922         Input
   1923         ------
   1924         coverage_map : bool
   1925             If set to `True`, then checks the scene in preparation for coverage
   1926             map computation. Otherwise, checks the scene in preparation for
   1927             paths computation.
   1928         """
   1929         if not self._rx_array:
   1930             raise ValueError("Receiver array not set.")
   1931 
   1932         if not self._tx_array:
   1933             raise ValueError("Transmitter array not set.")
   1934 
   1935         if len(self._transmitters) == 0:
   1936             raise ValueError("No transmitter defined.")
   1937 
   1938         # Instantiation of receivers is not needed to compute a coverage map
   1939         if not coverage_map:
   1940             if len(self._receivers) == 0:
   1941                 raise ValueError("No receiver defined.")
   1942 
   1943         # Check that all scene objects have a radio material
   1944         for obj in self.objects.values():
   1945             mat = obj.radio_material
   1946             if mat is None:
   1947                 msg = f"Scene object {obj.name} has no material set."
   1948                 raise ValueError(msg)
   1949             else:
   1950                 # Check that the material is well-defined
   1951                 if not mat.well_defined:
   1952                     msg = f"Material '{mat.name}' is used by the object "\
   1953                            f" '{obj.name}' but is not well-defined."
   1954                     raise ValueError(msg)
   1955                 # Check that the material is not a placeholder
   1956                 if mat.is_placeholder:
   1957                     msg = f"Material '{mat.name}' is used by the object "\
   1958                            f" '{obj.name}' but not defined."
   1959                     raise ValueError(msg)
   1960 
   1961     def _load_cameras(self):
   1962         """
   1963         Load the camera(s) available in the scene
   1964         """
   1965         for i, mi_cam in enumerate(self._scene.sensors()):
   1966             # Extract the transformation paramters
   1967             transform = mi.traverse(mi_cam)['to_world']
   1968             position = Camera.world_to_position(transform)
   1969             orientation = Camera.world_to_angles(transform)
   1970 
   1971             # Create the camera
   1972             name = f"scene-cam-{i}"
   1973             new_cam = Camera(name=name,
   1974                              position=position,
   1975                              orientation=orientation)
   1976             new_cam.scene = self
   1977 
   1978             self._cameras[name] = new_cam
   1979 
   1980     def _load_scene_objects(self):
   1981         """
   1982         Load the scene objects available in the scene
   1983         """
   1984 
   1985         # List of shapes indexed by their IDs
   1986         self._mi_shapes = self._scene.shapes()
   1987 
   1988         # Parse all shapes in the scene
   1989         for obj_id,s in enumerate(self._mi_shapes):
   1990             # Only meshes are handled
   1991             if not isinstance(s, mi.Mesh):
   1992                 raise TypeError('Only triangle meshes are supported')
   1993 
   1994             # Setup the material
   1995             mat_name = s.bsdf().id()
   1996             if mat_name.startswith("mat-"):
   1997                 mat_name = mat_name[4:]
   1998             mat = self.get(mat_name)
   1999             if (mat is not None) and (not isinstance(mat, RadioMaterial)):
   2000                 raise ValueError(f"Name'{name}' already used by another item")
   2001             elif mat is None:
   2002                 # If the radio material does not exist, then a placeholder is
   2003                 # used.
   2004                 mat = RadioMaterial(mat_name)
   2005                 mat.is_placeholder = True
   2006                 self._radio_materials[mat_name] = mat
   2007 
   2008             # Instantiate the scene objects
   2009             name = s.id()
   2010             if name.startswith('mesh-'):
   2011                 name = name[5:]
   2012             if self._is_name_used(name):
   2013                 raise ValueError(f"Name'{name}' already used by another item")
   2014             obj = SceneObject(name, object_id=obj_id, mi_shape=s, dtype=self._dtype)
   2015             obj.scene = self
   2016             obj.radio_material = mat_name
   2017 
   2018             self._scene_objects[name] = obj
   2019 
   2020     def _is_name_used(self, name):
   2021         """
   2022         Returns `True` if ``name`` is used by a scene object, a transmitter,
   2023         or a receiver.
   2024         """
   2025         used = ((name in self._transmitters)
   2026              or (name in self._receivers)
   2027              or (name in self._radio_materials)
   2028              or (name in self._scene_objects))
   2029         return used
   2030 
   2031 
   2032 def load_scene(filename=None, dtype=tf.complex64):
   2033     # pylint: disable=line-too-long
   2034     r"""
   2035     Load a scene from file
   2036 
   2037     Note that only one scene can be loaded at a time.
   2038 
   2039     Input
   2040     -----
   2041     filename : str
   2042         Name of a valid scene file. Sionna uses the simple XML-based format
   2043         from `Mitsuba 3 <https://mitsuba.readthedocs.io/en/stable/src/key_topics/scene_format.html>`_.
   2044         Defaults to `None` for which an empty scene is created.
   2045 
   2046     dtype : tf.complex
   2047         Dtype used for all internal computations and outputs.
   2048         Defaults to `tf.complex64`.
   2049 
   2050     Output
   2051     ------
   2052     scene : :class:`~sionna.rt.Scene`
   2053         Reference to the current scene
   2054     """
   2055     # Create empty scene using the reserved filename "__empty__"
   2056     if filename is None:
   2057         filename = "__empty__"
   2058     return Scene(filename, dtype=dtype)
   2059 
   2060 #
   2061 # Module variables for example scene files
   2062 #
   2063 floor_wall = str(files(scenes).joinpath("floor_wall/floor_wall.xml"))
   2064 # pylint: disable=C0301
   2065 """
   2066 Example scene containing a ground plane and a vertical wall
   2067 
   2068 .. figure:: ../figures/floor_wall.png
   2069    :align: center
   2070 """
   2071 
   2072 # pylint: disable=C0301
   2073 simple_street_canyon = str(files(scenes).joinpath("simple_street_canyon/simple_street_canyon.xml"))
   2074 """
   2075 Example scene containing a few rectangular building blocks and a ground plane
   2076 
   2077 .. figure:: ../figures/street_canyon.png
   2078    :align: center
   2079 """
   2080 
   2081 # pylint: disable=C0301
   2082 simple_street_canyon_with_cars = str(files(scenes).joinpath("simple_street_canyon_with_cars/simple_street_canyon_with_cars.xml"))
   2083 """
   2084 Example scene containing a few rectangular building blocks and a ground plane as well as some cars
   2085 
   2086 .. figure:: ../figures/street_canyon_with_cars.png
   2087    :align: center
   2088 """
   2089 
   2090 etoile = str(files(scenes).joinpath("etoile/etoile.xml"))
   2091 # pylint: disable=C0301
   2092 """
   2093 Example scene containing the area around the Arc de Triomphe in Paris
   2094 The scene was created with data downloaded from `OpenStreetMap <https://www.openstreetmap.org>`_ and
   2095 the help of `Blender <https://www.blender.org>`_ and the `Blender-OSM <https://github.com/vvoovv/blender-osm>`_
   2096 and `Mitsuba Blender <https://github.com/mitsuba-renderer/mitsuba-blender>`_ add-ons.
   2097 The data is licensed under the `Open Data Commons Open Database License (ODbL) <https://openstreetmap.org/copyright>`_.
   2098 
   2099 .. figure:: ../figures/etoile.png
   2100    :align: center
   2101 """
   2102 
   2103 munich = str(files(scenes).joinpath("munich/munich.xml"))
   2104 # pylint: disable=C0301
   2105 """
   2106 Example scene containing the area around the Frauenkirche in Munich
   2107 The scene was created with data downloaded from `OpenStreetMap <https://www.openstreetmap.org>`_ and
   2108 the help of `Blender <https://www.blender.org>`_ and the `Blender-OSM <https://github.com/vvoovv/blender-osm>`_
   2109 and `Mitsuba Blender <https://github.com/mitsuba-renderer/mitsuba-blender>`_ add-ons.
   2110 The data is licensed under the `Open Data Commons Open Database License (ODbL) <https://openstreetmap.org/copyright>`_.
   2111 
   2112 .. figure:: ../figures/munich.png
   2113    :align: center
   2114 """
   2115 
   2116 simple_wedge = str(files(scenes).joinpath("simple_wedge/simple_wedge.xml"))
   2117 # pylint: disable=C0301
   2118 r"""
   2119 Example scene containing a wedge with a :math:`90^{\circ}` opening angle
   2120 
   2121 .. figure:: ../figures/simple_wedge.png
   2122    :align: center
   2123 """
   2124 
   2125 simple_reflector = str(files(scenes).joinpath("simple_reflector/simple_reflector.xml"))
   2126 # pylint: disable=C0301
   2127 r"""
   2128 Example scene containing a metallic square
   2129 
   2130 .. figure:: ../figures/simple_reflector.png
   2131    :align: center
   2132 """
   2133 
   2134 double_reflector = str(files(scenes).joinpath("double_reflector/double_reflector.xml"))
   2135 # pylint: disable=C0301
   2136 r"""
   2137 Example scene containing two metallic squares
   2138 
   2139 .. figure:: ../figures/double_reflector.png
   2140    :align: center
   2141 """
   2142 
   2143 triple_reflector = str(files(scenes).joinpath("triple_reflector/triple_reflector.xml"))
   2144 # pylint: disable=C0301
   2145 r"""
   2146 Example scene containing three metallic rectangles
   2147 
   2148 .. figure:: ../figures/triple_reflector.png
   2149    :align: center
   2150 """
   2151 
   2152 box = str(files(scenes).joinpath("box/box.xml"))
   2153 # pylint: disable=C0301
   2154 r"""
   2155 Example scene containing a metallic box
   2156 
   2157 .. figure:: ../figures/box.png
   2158    :align: center
   2159 """