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 """