## SPDX-FileCopyrightText: Copyright (c) 2021-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.# SPDX-License-Identifier: Apache-2.0#"""Scene class, utilities, and example scenes"""from__future__importannotationsimportosfromimportlib_resourcesimportfilesfromcopyimportdeepcopyimportreimportxml.etree.ElementTreeasETfromtypingimportListimportdrjitasdrimportmatplotlibimportmatplotlib.pyplotaspltimportmitsubaasmiimportnumpyasnpfromscipy.constantsimportspeed_of_light,Boltzmannimportsionnafrom.constantsimportDEFAULT_FREQUENCY,DEFAULT_BANDWIDTH, \
DEFAULT_TEMPERATURE, \
DEFAULT_PREVIEW_BACKGROUND_COLOR, \
DEFAULT_THICKNESSfrom.radio_materialsimportRadioMaterialBasefrom.scene_objectimportSceneObjectfrom.antenna_arrayimportAntennaArrayfrom.cameraimportCamerafrom.previewimportPreviewerfrom.rendererimportrenderfrom.utilsimportradio_map_color_mappingfrom.radio_devicesimportTransmitter,Receiverfrom.importscenes
[docs]classScene:# pylint: disable=line-too-longr""" A scene contains everything that is needed for radio propagation simulation and rendering. A scene is a collection of multiple instances of :class:`~sionna.rt.SceneObject` which define the geometry and materials of the objects in the scene. It also includes transmitters (:class:`~sionna.rt.Transmitter`) and receivers (:class:`~sionna.rt.Receiver`). A scene is instantiated by calling :func:`~sionna.rt.load_scene()`. `Example scenes <https://nvlabs.github.io/sionna/rt/api/scene.html#examples>`_ can be loaded as follows: .. code-block:: Python from sionna.rt import load_scene scene = load_scene(sionna.rt.scene.munich) scene.preview() .. figure:: ../figures/scene_preview.png :align: center :param mi_scene: A Mitsuba scene """def__init__(self,mi_scene:mi.Scene|None=None):# Transmitter antenna arrayself._tx_array=None# Receiver antenna arrayself._rx_array=None# Radio materialsself._radio_materials={}# Scene objectsself._scene_objects={}# Radio devicesself._transmitters={}self._receivers={}# Preview widgetself._preview_widget=None# Set the frequency to the default valueself.frequency=DEFAULT_FREQUENCY# Set the bandwidth to the default valueself.bandwidth=DEFAULT_BANDWIDTH# Set the temperature to the default valueself.temperature=DEFAULT_TEMPERATURE# If no scene is loaded, then load the empty sceneifmi_sceneisNone:self._scene=mi.load_dict({"type":"scene"})else:assertisinstance(mi_scene,mi.Scene)self._scene=mi_sceneself._scene_params=mi.traverse(self._scene)# Load the scene objects.# The radio material is a Mitsuba BSDF, and as so were already# instantiated when loading the Mitsuba scene.# Note that when the radio material is instantiated, it is added# to the this scene.self._load_scene_objects()@propertydeffrequency(self):""" :py:class:`mi.Float` : Get/set the carrier frequency [Hz] """returnself._frequency@frequency.setterdeffrequency(self,f):iff<=0.0:raiseValueError("Frequency must be positive")self._frequency=mi.Float(f)# Update radio materialsformatinself.radio_materials.values():mat.frequency_update()@propertydefwavelength(self):""" :py:class:`mi.Float` : Wavelength [m] """returnspeed_of_light/self.frequency@propertydeftemperature(self):""" :py:class:`mi.Float` : Get/set the environment temperature [K]. Used for the computation of :attr:`~sionna.rt.Scene.thermal_noise_power`. """returnself._temperature@temperature.setterdeftemperature(self,v):ifv<0:raiseValueError("temperature must be positive")self._temperature=mi.Float(v)@propertydefbandwidth(self):""" :py:class:`mi.Float` : Get/set the transmission bandwidth [Hz]. Used for the computation of :attr:`~sionna.rt.Scene.thermal_noise_power`. """returnself._bandwidth@bandwidth.setterdefbandwidth(self,v):ifv<0:raiseValueError("bandwidth must be positive")self._bandwidth=mi.Float(v)@propertydefthermal_noise_power(self):""" :py:class:`mi.Float` : Thermal noise power [W] """returnself.temperature*Boltzmann*self.bandwidth@propertydefangular_frequency(self):""" :py:class:`mi.Float` : Angular frequency [rad/s] """returndr.two_pi*self.frequency@propertydeftx_array(self):""" :class:`~rt.AntennaArray` : Get/set the antenna array used by all transmitters in the scene """returnself._tx_array@tx_array.setterdeftx_array(self,array):ifnotisinstance(array,AntennaArray):raiseTypeError("`array` must be an instance of ``AntennaArray``")self._tx_array=array@propertydefrx_array(self):""" :class:`~rt.AntennaArray` : Get/set the antenna array used by all receivers in the scene """returnself._rx_array@rx_array.setterdefrx_array(self,array):ifnotisinstance(array,AntennaArray):raiseTypeError("`array` must be an instance of ``AntennaArray``")self._rx_array=array@propertydefradio_materials(self):""" :py:class:`dict`, { "name", :class:`~rt.RadioMaterialBase`} : Dictionary of radio materials """returndict(self._radio_materials)@propertydefobjects(self):""" :py:class:`dict`, { "name", :class:`~rt.SceneObject`} : Dictionary of scene objects """returndict(self._scene_objects)@propertydeftransmitters(self):""" :py:class:`dict`, { "name", :class:`~rt.Transmitter`} : Dictionary of transmitters """returndict(self._transmitters)@propertydefreceivers(self):""" :py:class:`dict`, { "name", :class:`~rt.Receiver`} : Dictionary of receivers """returndict(self._receivers)@propertydefpaths_solver(self):""" :class:`rt.PathSolverBase` : Get/set the path solver """returnself._paths_solver@paths_solver.setterdefpaths_solver(self,solver):self._paths_solver=solver
[docs]defget(self,name:str)->(None|sionna.rt.RadioDevice|sionna.rt.RadioMaterialBase):# pylint: disable=line-too-longr""" Returns a scene object, radio device, or radio material :param name: Name of the item to retrieve """ifnameinself._radio_materials:returnself._radio_materials[name]ifnameinself._scene_objects:returnself._scene_objects[name]ifnameinself._transmitters:returnself._transmitters[name]ifnameinself._receivers:returnself._receivers[name]returnNone
[docs]defadd(self,item:(sionna.rt.RadioDevice|sionna.rt.RadioMaterialBase))->None:# pylint: disable=line-too-longr""" Adds a radio device or radio material to the scene If a different item with the same name as ``item`` is part of the scene, an error is raised. :param item: Item to be added to the scene """name=item.names_item=self.get(name)ifs_itemisnotNone:ifs_itemisnotitem:raiseValueError(f"Name '{name}' is already used by another item"" of the scene")# This exact item was already added, skip it.returnifisinstance(item,RadioMaterialBase):item.scene=selfself._radio_materials[name]=itemelifisinstance(item,Transmitter):self._transmitters[name]=itemelifisinstance(item,Receiver):self._receivers[name]=itemelse:raiseValueError(f"Cannot add object of type {type(item)} to the scene."" The input must be a Transmitter, Receiver,"" or RadioMaterialBase.")
[docs]defremove(self,name:str)->None:# pylint: disable=line-too-long""" Removes a radio device or radio material from the scene In the case of a radio material, it must not be used by any object of the scene. :param name: Name of the item to be removed """ifnotisinstance(name,str):raiseValueError("The input should be a string")item=self.get(name)ifitemisNone:passelifisinstance(item,RadioMaterialBase):ifitem.is_used:raiseValueError(f"Cannot remove the radio material '{name}'"" because it is still used by at least one"" object")delself._radio_materials[name]elifisinstance(item,Transmitter):delself._transmitters[name]elifisinstance(item,Receiver):delself._receivers[name]else:raiseTypeError("Only Transmitters, Receivers, or RadioMaterials"" can be removed")
[docs]defedit(self,add:(sionna.rt.SceneObject|list[sionna.rt.SceneObject]|dict|None)=None,remove:(str|sionna.rt.SceneObject|list[sionna.rt.SceneObject|str]|None)=None)->None:r""" Add and/or remove a list of objects to/from the scene To optimize performance and reduce processing time, it is recommended to use a single call to this function with a list of objects to add and/or remove, rather than making multiple individual calls to edit scene objects. :param add: Object, or list /dictionary of objects to be added :param remove: Name or object, or list/dictionary of objects or names to be added """# Set the Mitsuba scene to the edited sceneself._scene=edit_scene_shapes(self,add=add,remove=remove)# Reset the scene paramsself._scene_params=mi.traverse(self._scene)# Update the scene objects.# Scene objects are not re-instantiated to keep the instances hold by# the users validscene_objects=self._scene_objectsifaddisnotNone:ifisinstance(add,SceneObject):add=[add]scene_objects.update({o.name:oforoinadd})self._scene_objects={}forsinself._scene.shapes():name=SceneObject.shape_id_to_name(s.id())obj=scene_objects.get(name)assertobjobj.mi_shape=sself._add_scene_object(obj)# Reset the preview widget to ensure the preview is redrawself.scene_geometry_updated()
[docs]defpreview(self,*,background:str=DEFAULT_PREVIEW_BACKGROUND_COLOR,clip_at:float|None=None,clip_plane_orientation:tuple[float,float,float]=(0,0,-1),fov:float=45.,paths:sionna.rt.Paths|None=None,radio_map:sionna.rt.RadioMap|None=None,resolution:tuple[int,int]=(655,500),rm_db_scale:bool=True,rm_metric:str="path_gain",rm_tx:int|str|None=None,rm_vmax:float|None=None,rm_vmin:float|None=None,show_devices:bool=True,show_orientations:bool=False)->sionna.rt.preview.Previewer:# pylint: disable=line-too-longr"""In an interactive notebook environment, opens an interactive 3D viewer of the scene. Default color coding: * Green: Receiver * Blue: Transmitter Controls: * Mouse left: Rotate * Scroll wheel: Zoom * Mouse right: Move :param background: Background color in hex format prefixed by "#" :param clip_at: If not `None`, the scene preview will be clipped (cut) by a plane with normal orientation ``clip_plane_orientation`` and offset ``clip_at``. That means that everything *behind* the plane becomes invisible. This allows visualizing the interior of meshes, such as buildings. :param clip_plane_orientation: Normal vector of the clipping plane :param fov: Field of view [deg] :param paths: Optional propagation paths to be shown :param radio_map: Optional radio map to be shown :param resolution: Size of the viewer figure :param rm_db_scale: Use logarithmic scale for radio map visualization, i.e. the radio map values are mapped to: :math:`y = 10 \cdot \log_{10}(x)`. :param rm_metric: Metric of the radio map to be displayed :type rm_metric: "path_gain" | "rss" | "sinr" :param rm_tx: When ``radio_map`` is specified, controls for which of the transmitters the radio map is shown. Either the transmitter's name or index can be given. If `None`, the maximum metric over all transmitters is shown. :param rm_vmax: For radio map visualization, defines the maximum value that the colormap covers. It should be provided in dB if ``rm_db_scale`` is set to `True`, or in linear scale otherwise. :param rm_vmin: For radio map visualization, defines the minimum value that the colormap covers. It should be provided in dB if ``rm_db_scale`` is set to `True`, or in linear scale otherwise. :param show_devices: Show radio devices :param show_orientations: Show orientation of radio devices """if(self._preview_widgetisnotNone)and(resolutionisnotNone):assertisinstance(resolution,(tuple,list))andlen(resolution)==2iftuple(resolution)!=self._preview_widget.resolution:# User requested a different rendering resolution, create# a new viewer from scratch to match it.self._preview_widget=None# Cache the render widget so that we don't need to re-create it# every timefig=self._preview_widgetneeds_reset=figisnotNoneshow_paths=pathsisnotNoneifneeds_reset:fig.reset()else:fig=Previewer(scene=self,resolution=resolution,fov=fov,background=background)self._preview_widget=fig# Show paths and devices, if requiredifshow_paths:fig.plot_paths(paths)ifshow_devices:fig.plot_radio_devices(show_orientations=show_orientations)ifradio_mapisnotNone:fig.plot_radio_map(radio_map,tx=rm_tx,db_scale=rm_db_scale,vmin=rm_vmin,vmax=rm_vmax,metric=rm_metric)# Show legendifshow_pathsorshow_devices:fig.show_legend(show_paths=show_paths,show_devices=show_devices)# Clippingfig.set_clipping_plane(offset=clip_at,orientation=clip_plane_orientation)# Update the camera stateifnotneeds_reset:fig.center_view()returnfig
[docs]defrender(self,*,camera:Camera|str,clip_at:float|None=None,clip_plane_orientation:tuple[float,float,float]=(0,0,-1),envmap:str|None=None,fov:float=45,lighting_scale:float=1.0,num_samples:int=128,paths:sionna.rt.Paths|None=None,radio_map:sionna.rt.RadioMap|None=None,resolution:tuple[int,int]=(655,500),return_bitmap:bool=False,rm_db_scale:bool=True,rm_metric:str="path_gain",rm_show_color_bar:bool=False,rm_tx:int|str|None=None,rm_vmax:float|None=None,rm_vmin:float|None=None,show_devices:bool=True)->plt.Figure|mi.Bitmap:# pylint: disable=line-too-longr"""Renders the scene from the viewpoint of a camera or the interactive viewer :param camera: Camera to be used for rendering the scene. If an interactive viewer was opened with :meth:`~sionna.rt.Scene.preview()`, `"preview"` can be to used to render the scene from its viewpoint. :param clip_at: If not `None`, the scene preview will be clipped (cut) by a plane with normal orientation ``clip_plane_orientation`` and offset ``clip_at``. That means that everything *behind* the plane becomes invisible. This allows visualizing the interior of meshes, such as buildings. :param clip_plane_orientation: Normal vector of the clipping plane :param envmap: Path to an environment map image file (e.g. in EXR format) to use for scene lighting :param fov: Field of view [deg] :param lighting_scale: Scale to apply to the lighting in the scene (e.g., from a constant uniform emitter or a given environment map) :param num_samples: Number of rays thrown per pixel :param paths: Optional propagation paths to be shown :param radio_map: Optional radio map to be shown :param resolution: Size of the viewer figure :param return_bitmap: If `True`, directly return the rendered image :param rm_db_scale: Use logarithmic scale for radio map visualization, i.e. the radio map values are mapped to: :math:`y = 10 \cdot \log_{10}(x)`. :param rm_metric: Metric of the radio map to be displayed :type rm_metric: "path_gain" | "rss" | "sinr" :param rm_show_color_bar: Show color bar :param rm_tx: When ``radio_map`` is specified, controls for which of the transmitters the radio map is shown. Either the transmitter's name or index can be given. If `None`, the maximum metric over all transmitters is shown. :param rm_vmax: For radio map visualization, defines the maximum value that the colormap covers. It should be provided in dB if ``rm_db_scale`` is set to `True`, or in linear scale otherwise. :param rm_vmin: For radio map visualization, defines the minimum value that the colormap covers. It should be provided in dB if ``rm_db_scale`` is set to `True`, or in linear scale otherwise. :param show_devices: Show radio devices """image=render(scene=self,camera=camera,paths=paths,show_devices=show_devices,clip_at=clip_at,clip_plane_orientation=clip_plane_orientation,radio_map=radio_map,rm_tx=rm_tx,rm_db_scale=rm_db_scale,rm_vmin=rm_vmin,rm_vmax=rm_vmax,rm_metric=rm_metric,num_samples=num_samples,resolution=resolution,fov=fov,envmap=envmap,lighting_scale=lighting_scale)ifreturn_bitmap:returnimageto_show=image.convert(component_format=mi.Struct.Type.UInt8,srgb_gamma=True)show_color_bar=(radio_mapisnotNone)andrm_show_color_barifshow_color_bar:aspect=image.width()*1.06/image.height()fig,ax=plt.subplots(1,2,gridspec_kw={'width_ratios':[0.97,0.03]},figsize=(aspect*6,6))im_ax=ax[0]else:aspect=image.width()/image.height()fig,ax=plt.subplots(1,1,figsize=(aspect*6,6))im_ax=axim_ax.imshow(to_show)ifshow_color_bar:cm=getattr(radio_map,rm_metric).numpy()ifrm_txisNone:cm=np.max(cm,axis=0)else:cm=cm[rm_tx]# Ensure that dBm is correctly computed for RSSifrm_metric=="rss"andrm_db_scale:cm*=1000_,normalizer,color_map=radio_map_color_mapping(cm,db_scale=rm_db_scale,vmin=rm_vmin,vmax=rm_vmax)mappable=matplotlib.cm.ScalarMappable(norm=normalizer,cmap=color_map)cax=ax[1]ifrm_metric=="rss"andrm_db_scale:cax.set_title("dBm")else:cax.set_title('dB')fig.colorbar(mappable,cax=cax)# Remove axes and marginsim_ax.axis('off')fig.tight_layout()returnfig
[docs]defrender_to_file(self,*,camera:Camera|str,filename:str,clip_at:float|None=None,clip_plane_orientation:tuple[float,float,float]=(0,0,-1),envmap:str|None=None,fov:float=45,lighting_scale:float=1.0,num_samples:int=512,paths:sionna.rt.Paths|None=None,radio_map:sionna.rt.RadioMap|None=None,resolution:tuple[int,int]=(655,500),rm_db_scale:bool=True,rm_metric:str="path_gain",rm_tx:int|str|None=None,rm_vmin:float|None=None,rm_vmax:float|None=None,show_devices:bool=True)->mi.Bitmap:# pylint: disable=line-too-longr"""Renders the scene from the viewpoint of a camera or the interactive viewer, and saves the resulting image :param camera: Camera to be used for rendering the scene. If an interactive viewer was opened with :meth:`~sionna.rt.Scene.preview()`, `"preview"` can be to used to render the scene from its viewpoint. :param filename: Filename for saving the rendered image, e.g., "my_scene.png" :param clip_at: If not `None`, the scene preview will be clipped (cut) by a plane with normal orientation ``clip_plane_orientation`` and offset ``clip_at``. That means that everything *behind* the plane becomes invisible. This allows visualizing the interior of meshes, such as buildings. :param clip_plane_orientation: Normal vector of the clipping plane :param envmap: Path to an environment map image file (e.g. in EXR format) to use for scene lighting :param fov: Field of view [deg] :param lighting_scale: Scale to apply to the lighting in the scene (e.g., from a constant uniform emitter or a given environment map) :param num_samples: Number of rays thrown per pixel :param paths: Optional propagation paths to be shown :param radio_map: Optional radio map to be shown :param resolution: Size of the viewer figure :param rm_db_scale: Use logarithmic scale for radio map visualization, i.e. the radio map values are mapped to: :math:`y = 10 \cdot \log_{10}(x)`. :param rm_metric: Metric of the radio map to be displayed :type rm_metric: "path_gain" | "rss" | "sinr" :param rm_tx: When ``radio_map`` is specified, controls for which of the transmitters the radio map is shown. Either the transmitter's name or index can be given. If `None`, the maximum metric over all transmitters is shown. :param rm_vmax: For radio map visualization, defines the maximum value that the colormap covers. It should be provided in dB if ``rm_db_scale`` is set to `True`, or in linear scale otherwise. :param rm_vmin: For radio map visualization, defines the minimum value that the colormap covers. It should be provided in dB if ``rm_db_scale`` is set to `True`, or in linear scale otherwise. :param show_devices: Show radio devices """image=render(scene=self,camera=camera,paths=paths,show_devices=show_devices,clip_at=clip_at,clip_plane_orientation=clip_plane_orientation,radio_map=radio_map,rm_tx=rm_tx,rm_db_scale=rm_db_scale,rm_vmin=rm_vmin,rm_vmax=rm_vmax,rm_metric=rm_metric,num_samples=num_samples,resolution=resolution,fov=fov,envmap=envmap,lighting_scale=lighting_scale)ext=os.path.splitext(filename)[1].lower()ifextin('.jpg','.jpeg','.ppm',):image=image.convert(component_format=mi.Struct.Type.UInt8,pixel_format=mi.Bitmap.PixelFormat.RGB,srgb_gamma=True)elifextin('.png','.tga''.bmp'):image=image.convert(component_format=mi.Struct.Type.UInt8,srgb_gamma=True)image.write(filename)returnimage
@propertydefmi_scene_params(self):r""" :py:class:`mi.SceneParameters` : Mitsuba scene parameters """returnself._scene_params@propertydefmi_scene(self):r""" :py:class:`mi.Scene` : Mitsuba scene """returnself._scene# pylint: disable=line-too-long
[docs]defsources(self,synthetic_array:bool,return_velocities:bool)->tuple[mi.Point3f,mi.Point3f,mi.Point3f|None,mi.Vector3f|None]:r""" Builds arrays containing the positions and orientations of the sources If synthetic arrays are not used, then every transmit antenna is modeled as a source of paths. Otherwise, transmitters are modelled as if they had a single antenna located at their :attr:`~sionna.rt.RadioDevice.position`. :return: Positions of the sources :return: Orientations of the sources :return: Positions of the antenna elements relative to the transmitters positions. `None` is returned if ``synthetic_array`` is `True`. :return: Velocities of the transmitters. `None` is returned if `return_velocities` is set to `False`. """returnself._endpoints(self.transmitters.values(),self.tx_array,synthetic_array,return_velocities)
# pylint: disable=line-too-long
[docs]deftargets(self,synthetic_array:bool,return_velocities:bool,)->tuple[mi.Point3f,mi.Point3f,mi.Point3f|None,mi.Vector3f|None]:r""" Builds arrays containing the positions and orientations of the targets If synthetic arrays are not used, then every receiver antenna is modeled as a source of paths. Otherwise, receivers are modelled as if they had a single antenna located at their :attr:`~sionna.rt.RadioDevice.position`. :return: Positions of the targets :return: Orientations of the targets :return: Positions of the antenna elements relative to the receivers. Only returned if ``synthetic_array`` is `True`. :return: Velocities of the transmitters. `None` is returned if `return_velocities` is set to `False`. """returnself._endpoints(self.receivers.values(),self.rx_array,synthetic_array,return_velocities)
[docs]defscene_geometry_updated(self)->None:""" Callback to trigger when the scene geometry is updated """# Update the scene geometry in the previewifself._preview_widget:self._preview_widget.redraw_scene_geometry()
[docs]defall_set(self,radio_map:bool)->None:# pylint: disable=line-too-longr""" Raises an exception if the scene is not all set for simulations :param radio_map: Set to `True` if checking for radio map computation. Set to `False` otherwise. """ifself.tx_arrayisNone:raiseValueError("Transmitter array not set")iflen(self.transmitters)==0:raiseValueError("Scene has no transmitters")ifnotradio_map:ifself.rx_arrayisNone:raiseValueError("Receiver array not set")iflen(self.receivers)==0:raiseValueError("Scene has no receivers")
################################################### Internal methods##################################################def_load_scene_objects(self):""" Builds Sionna SceneObject instances from the Mistuba scene """# List of shapesshapes=self._scene.shapes()# Parse all shapes in the sceneforsinshapes:# Only meshes are supportedifnotisinstance(s,mi.Mesh):raiseTypeError('Only triangle meshes are supported')# Instantiate the scene objectscene_object=SceneObject(mi_shape=s)# Add a scene object to the sceneself._add_scene_object(scene_object)def_add_scene_object(self,scene_object:sionna.rt.SceneObject)->None:r""" Add `scene_object` to the scene. Note that this function does not add the object to the Mitsuba scene, just to the Sionna wrapper. :param scene_object: Object to add """ifnotisinstance(scene_object,SceneObject):raiseValueError("The input must be a SceneObject")name=scene_object.names_item=self.get(name)ifs_itemisnotNone:ifs_itemisnotscene_object:raiseValueError(f"Name '{name}' is already used by another item"" of the scene")else:# This item was already added.return# Add the scene object and its materialifnotscene_object.radio_material:raiseValueError(f"Object {scene_object.name} has no radio"" material assigned to it")scene_object.scene=selfself._scene_objects[scene_object.name]=scene_object# Add the scene object radio material as wellself.add(scene_object.radio_material)def_is_name_used(self,name:str)->bool:""" Returns `True` if ``name`` is used by a scene object, a transmitter, a receiver, or a radio material. Returns `False` otherwise. :param name: Name """used=((nameinself._radio_materials)or(nameinself._scene_objects)or(nameinself._transmitters)or(nameinself._receivers))returnuseddef_endpoints(self,radio_devices:List[mi.Transmitter|mi.Receiver],array:AntennaArray,synthetic_array:bool,return_velocities:bool)->tuple[mi.Point3f,mi.Point3f,mi.Point3f|None,mi.Vector3f|None]:r""" Builds arrays containing the positions and orientations of the endpoints (sources or targets) If synthetic arrays are not used, then every antenna is modeled as an endpoint of paths. Otherwise, radio devices are modelled as if they had a single antenna located at their :attr:`~sionna.rt.RadioDevice.position`. :param radio_devices: List of radio devices, i.e., transmitters or receivers :param array: Antenna array used by the radio devices :param synthetic_array: Flag indicating if a synthetic array is used :param return_velocities: If set to `True`, then the velocities of the radio devices are returned :return: Positions of the endpoints :return: Orientations of the endpoints :return: Positions of the antenna elements relative to the endpoints positions. `None` is returned if ``synthetic_array`` is `True`. :return: Velocities of the radio devices. `None` is returned if `return_velocities` is set to `False`. """n_dev=len(radio_devices)ifsynthetic_arrayor(arrayisNone):n_ep=n_deveff_array_size_src=1else:n_ep=n_dev*array.array_sizeeff_array_size_src=array.array_sizepositions=dr.zeros(mi.Point3f,n_ep)orientations=dr.zeros(mi.Point3f,n_ep)ifsynthetic_array:rel_ant_positions=dr.zeros(mi.Point3f,n_ep*array.array_size)rel_and_ind=dr.arange(mi.UInt,array.array_size)else:rel_ant_positions=Noneifreturn_velocities:velocities=dr.zeros(mi.Vector3f,n_dev)else:velocities=Nones=dr.arange(mi.UInt,eff_array_size_src)fori,devinenumerate(radio_devices):p=dev.positiono=dev.orientationv=dev.velocityifsynthetic_array:p_ant=array.rotate(self.wavelength,o)ind=rel_and_ind+i*array.array_sizedr.scatter(rel_ant_positions.x,p_ant.x,ind)dr.scatter(rel_ant_positions.y,p_ant.y,ind)dr.scatter(rel_ant_positions.z,p_ant.z,ind)else:p=array.rotate(self.wavelength,o)+pind=s+i*eff_array_size_srcdr.scatter(positions.x,p.x,ind)dr.scatter(positions.y,p.y,ind)dr.scatter(positions.z,p.z,ind)#dr.scatter(orientations.x,o.x,ind)dr.scatter(orientations.y,o.y,ind)dr.scatter(orientations.z,o.z,ind)#ifreturn_velocities:dr.scatter(velocities.x,v.x,i)dr.scatter(velocities.y,v.y,i)dr.scatter(velocities.z,v.z,i)returnpositions,orientations,rel_ant_positions,velocities
[docs]defload_scene(filename:str|None=None,merge_shapes:bool=True,merge_shapes_exclude_regex:str|None=None)->Scene:# pylint: disable=line-too-longr""" Loads a scene from file :param filename: Name of a valid scene file. Sionna uses the simple XML-based format from `Mitsuba 3 <https://mitsuba.readthedocs.io/en/stable/src/key_topics/scene_format.html>`_. For `None`, an empty scene is created. :param merge_shapes: If set to `True`, shapes that share the same radio material are merged. :param merge_shapes_exclude_regex: Optional regex to exclude shapes from merging. Only used if ``merge_shapes`` is set to `True`. """iffilenameisNone:returnScene()processed=process_xml(filename,merge_shapes=merge_shapes,merge_shapes_exclude_regex=merge_shapes_exclude_regex)# Since we will be loading directly from a string,# we have to make sure the directory containing the scene# is part of the search path for meshes, etc.thread=mi.Thread.thread()fres_old=thread.file_resolver()fres=mi.FileResolver(fres_old)fres.append(os.path.dirname(filename))try:thread.set_file_resolver(fres)mi_scene=mi.load_string(processed)finally:thread.set_file_resolver(fres_old)returnScene(mi_scene=mi_scene)
defprocess_xml(fname:str,merge_shapes:bool=True,merge_shapes_exclude_regex:str|None=None,default_thickness:float=DEFAULT_THICKNESS)->str:""" Preprocess the XML string describing the scene This function adds radio material holders to allow users to change the material describing a scene, and add an instruction to merge shapes that share the same radio material to speed-up ray tracing. :param fname: Filename to load :param merge_shapes: If set to `True`, shapes that share the same radio material are merged. :param merge_shapes_exclude_regex: Optional regex to exclude shapes from merging. Only used if ``merge_shapes`` is set to `True`. :param default_thickness: Default thickness [m] of radio materials """# Compile the regex if not 'None'ifmerge_shapes_exclude_regexisnotNone:regex=re.compile(merge_shapes_exclude_regex)else:regex=Noneradio_bsdf_types={"radio-material","itu-radio-material","holder-material",}tree=ET.parse(fname)root=tree.getroot()# 1. Replace BSDFs with radio BSDFs# We don't need to process BSDFs nested in other BSDFs, e.g. a `diffuse`# inside of a `twosided`. We just process the outermost BSDF element.forbsdfinroot.findall("./bsdf")+root.findall(".//shape/bsdf"):bsdf_type=bsdf.attrib.get("type")ifbsdf_typenotinradio_bsdf_types:mat_id=bsdf.attrib.get("id")bsdf_type="radio-material"props={"thickness":("float",default_thickness),}# If it's a known material, pre-fill the right propertiesname=mat_idifname.startswith("mat-"):name=name[4:]ifname.startswith("itu_"):bsdf_type="itu-radio-material"props["type"]=("string",name[4:])else:# Otherwise, we didn't recognize the name so we don't do# anything special with it.pass# TODO: we could consider saving some information about the original# "visual" BSDFs if that allows users to customize the look of their# scenes easily from Blender.bsdf.clear()bsdf.attrib["type"]=bsdf_typeifmat_idisnotNone:bsdf.attrib["id"]=mat_idfork,(t,v)inprops.items():bsdf.append(ET.Element(t,{"name":k,"value":str(v)}))# All BSDFs must be wrapped into a `HolderMaterial` so that we can let# the user easily swap BSDFs later on.ifbsdf_type!="holder-material":inner=deepcopy(bsdf)bsdf.clear()bsdf.attrib["type"]="holder-material"if"id"ininner.attrib:bsdf.attrib["id"]="holder-"+inner.attrib["id"]bsdf.append(inner)# 2. Wrap shapes into a `merge` shape if requested. Shapes that are# not merged are assigned indivdial material holder to allow setting their# radio material individually.merge=ET.Element("shape",{"type":"merge","id":"merged-shapes"})merge_node_empty=Trueforshapeinroot.findall("shape"):# Shape idshape_id=shape.attrib.get("id")# BSDF noderm_name=Nonerm_ref=None# Check of inner-BSDFforbsdfinshape.findall('bsdf'):assertrm_nameisNone,\
"Only a single BSDF can be assigned to a shape"assertbsdf.attrib["type"]=="holder-material"rm_node=bsdf.find("bsdf")rm_name=rm_node.attrib["id"]# Check for reference to BSDFforrm_refinshape.findall('ref'):ifrm_ref.get('name')=='bsdf':assertrm_nameisNone,\
"Only a single BSDF can be assigned to a shape"rm_name=rm_ref.attrib.get("id")#if(notmerge_shapes)or((merge_shapes_exclude_regexisnotNone)andregex.search(shape.attrib.get("id",""))):# Create an individual radio material holder for this shapebsdf_holder=ET.SubElement(shape,'bsdf',{'type':'holder-material','id':f'mat-holder-{shape_id}'})ET.SubElement(bsdf_holder,'ref',{"name":"bsdf","id":rm_name})# Remove the reference to the radio materialifrm_refisnotNone:shape.remove(rm_ref)else:# Use the generic radio material holderrm_ref.attrib["id"]="holder-"+rm_name# Add the shape to the merge noderoot.remove(shape)merge.append(shape)#merge_node_empty=Falseifnotmerge_node_empty:root.append(merge)ET.indent(root,space=" ")returnET.tostring(root).decode("utf-8")defedit_scene_shapes(scene:Scene,add:(sionna.rt.SceneObject|list[sionna.rt.SceneObject]|dict|None)=None,remove:(str|sionna.rt.SceneObject|list[sionna.rt.SceneObject|str]|None)=None,return_dict:bool=False)->dict|mi.Scene:""" Builds a *new* Mitsuba Scene object identicaly to `scene`, but which includes the shapes listed in `add` but not the shapes listed in `remove`. The shapes and other plugins that are left untouched carry over to the new scene (same objects). :param scene: Scene to edit :param add: Object, or list /dictionary of objects to be added :param remove: Name or object, or list/dictionary of objects or names to be added :param return_dict: If `True`, then the new scene is returned as a dictionnary. Otherwise, it is returned as a Mitsuba scene. """mi_scene=scene.mi_scene# Result scene as a dictresult={"type":"scene",}# Local utility to add an object to `result`defadd_with_id(obj,fallback_id):ifobjisNone:returnkey=obj.id()orfallback_idassertkeynotinresultresult[key]=obj# Add to `result` the visual components of the scene: sensors, integrator,# and environment mapfori,sensorinenumerate(mi_scene.sensors()):add_with_id(sensor,f"sensor-{i}")add_with_id(mi_scene.environment(),"envmap")add_with_id(mi_scene.integrator(),"integrator")# Build the sets of object ids to remove# Set of object ids to removeids_to_remove=set()# In case some given `Shape` objects don't have IDs, we keep a separate setother_to_remove=set()ifremoveisnotNone:ifisinstance(remove,(mi.Shape,str,SceneObject)):remove=[remove]forvinremove:ifisinstance(v,SceneObject):v=v.mi_shapeifisinstance(v,str):o=scene.objects.get(v)ifo:mi_id=o.mi_shape.id()ids_to_remove.add(mi_id)elifisinstance(v,mi.Shape):v_id=v.id()ifv_id:ids_to_remove.add(v_id)else:# Shape doesn't have an ID, we still want to remove itother_to_remove.add(v)else:raiseValueError(f"Cannot remove object of type ({type(v)})."" The `remove` argument should be a list"" containing either shape instances or shape"" IDs.")# Add to `result` all shapes of the current scene, except the ones we want# to excluden_shapes=0forshapeinmi_scene.shapes():shape_id=shape.id()if(shape_idinids_to_remove)or(shapeinother_to_remove):continueifnotshape_id:shape_id=f"shape-{n_shapes}"assertshape_idnotinresultresult[shape_id]=shapen_shapes+=1# Add the objects provided by the user though `add` to `result`ifaddisnotNone:ifisinstance(add,(mi.Object,dict,SceneObject)):add=[add]forainadd:ifisinstance(a,SceneObject):a=a.mi_shapeifisinstance(a,dict):new_id=a.get("id")elifisinstance(a,mi.Object):new_id=a.id()else:raiseValueError(f"Cannot add object of type ({type(a)})."" The `add` argument should be a list"" containing either a dict to be loaded by"" `mi.load_dict()` or an existing Mitsuba"" object instance.")ifnotnew_id:ifisinstance(a,mi.Shape):new_id=f"shape-{n_shapes}"n_shapes+=1else:new_id=f"object-{len(result)}"ifnew_idinresult:raiseValueError(f"Cannot add object of type ({type(a)}) with"f" ID \"{new_id}\" because this ID is already"" used in the scene.")result[new_id]=aifreturn_dict:returnresultelse:returnmi.load_dict(result)## Module variables for example scene files#box=str(files(scenes).joinpath("box/box.xml"))# pylint: disable=C0301"""Example scene containing a metallic box.. figure:: ../figures/box.png :align: center"""simple_reflector=str(files(scenes).joinpath("simple_reflector/simple_reflector.xml"))# pylint: disable=C0301"""Example scene containing a metallic reflector.. figure:: ../figures/simple_reflector.png :align: center"""simple_wedge=str(files(scenes).joinpath("simple_wedge/simple_wedge.xml"))# pylint: disable=C0301r"""Example scene containing a wedge with a :math:`90^{\circ}` opening angle.. figure:: ../figures/simple_wedge.png :align: center"""box_one_screen=str(files(scenes).joinpath("box_one_screen/box_one_screen.xml"))# pylint: disable=C0301"""Example scene containing a metallic box and a screen made of glassNote: In the figure below, the upper face of the box has been removed forvisualization purposes. In the actual scene, the box is closed on all sides... figure:: ../figures/box_one_screen.png :align: center"""box_two_screens=str(files(scenes).joinpath("box_two_screens/box_two_screens.xml"))# pylint: disable=C0301"""Example scene containing a metallic box and two screens made of glassNote: In the figure below, the upper face of the box has been removed forvisualization purposes. In the actual scene, the box is closed on all sides... figure:: ../figures/box_two_screens.png :align: center"""etoile=str(files(scenes).joinpath("etoile/etoile.xml"))# pylint: disable=C0301"""Example scene containing the area around the Arc de Triomphe in ParisThe scene was created with data downloaded from `OpenStreetMap <https://www.openstreetmap.org>`_ andthe help of `Blender <https://www.blender.org>`_ and the `Blender-OSM <https://github.com/vvoovv/blender-osm>`_and `Mitsuba Blender <https://github.com/mitsuba-renderer/mitsuba-blender>`_ add-ons.The data is licensed under the `Open Data Commons Open Database License (ODbL) <https://openstreetmap.org/copyright>`_... figure:: ../figures/etoile.png :align: center"""munich=str(files(scenes).joinpath("munich/munich.xml"))# pylint: disable=C0301"""Example scene containing the area around the Frauenkirche in MunichThe scene was created with data downloaded from `OpenStreetMap <https://www.openstreetmap.org>`_ andthe help of `Blender <https://www.blender.org>`_ and the `Blender-OSM <https://github.com/vvoovv/blender-osm>`_and `Mitsuba Blender <https://github.com/mitsuba-renderer/mitsuba-blender>`_ add-ons.The data is licensed under the `Open Data Commons Open Database License (ODbL) <https://openstreetmap.org/copyright>`_... figure:: ../figures/munich.png :align: center"""florence=str(files(scenes).joinpath("florence/florence.xml"))# pylint: disable=C0301"""Example scene containing the area around the Florence Cathedral in FlorenceThe scene was created with data downloaded from `OpenStreetMap <https://www.openstreetmap.org>`_ andthe help of `Blender <https://www.blender.org>`_ and the `Blender-OSM <https://github.com/vvoovv/blender-osm>`_and `Mitsuba Blender <https://github.com/mitsuba-renderer/mitsuba-blender>`_ add-ons.The data is licensed under the `Open Data Commons Open Database License (ODbL) <https://openstreetmap.org/copyright>`_... figure:: ../figures/florence.png :align: center"""low_poly_car=str(files(scenes).joinpath("low_poly_car.ply"))# pylint: disable=C0301"""Simple mesh of a car.. figure:: ../figures/low_poly_car.png :align: center"""floor_wall=str(files(scenes).joinpath("floor_wall/floor_wall.xml"))# pylint: disable=C0301"""Example scene containing a ground plane and a vertical wall.. figure:: ../figures/floor_wall.png :align: center"""# pylint: disable=C0301simple_street_canyon=str(files(scenes).joinpath("simple_street_canyon/simple_street_canyon.xml"))"""Example scene containing a few rectangular building blocks and a ground plane.. figure:: ../figures/street_canyon.png :align: center"""# pylint: disable=C0301simple_street_canyon_with_cars=str(files(scenes).joinpath("simple_street_canyon_with_cars/simple_street_canyon_with_cars.xml"))"""Example scene containing a few rectangular building blocks and a ground plane as well as some cars.. figure:: ../figures/street_canyon_with_cars.png :align: center"""double_reflector=str(files(scenes).joinpath("double_reflector/double_reflector.xml"))# pylint: disable=C0301r"""Example scene containing two metallic squares.. figure:: ../figures/double_reflector.png :align: center"""triple_reflector=str(files(scenes).joinpath("triple_reflector/triple_reflector.xml"))# pylint: disable=C0301r"""Example scene containing three metallic rectangles.. figure:: ../figures/triple_reflector.png :align: center"""