radio_material.py (12955B)
1 # 2 # SPDX-FileCopyrightText: Copyright (c) 2021-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. 3 # SPDX-License-Identifier: Apache-2.0 4 # 5 """ 6 Implements a radio material. 7 A radio material provides the EM radio properties for a specific material. 8 """ 9 10 import tensorflow as tf 11 12 from . import scene 13 from sionna.constants import DIELECTRIC_PERMITTIVITY_VACUUM, PI 14 from .scattering_pattern import ScatteringPattern, LambertianPattern 15 16 class RadioMaterial: 17 # pylint: disable=line-too-long 18 r""" 19 Class implementing a radio material 20 21 A radio material is defined by its relative permittivity 22 :math:`\varepsilon_r` and conductivity :math:`\sigma` (see :eq:`eta`), 23 as well as optional parameters related to diffuse scattering, such as the 24 scattering coefficient :math:`S`, cross-polarization discrimination 25 coefficient :math:`K_x`, and scattering pattern :math:`f_\text{s}(\hat{\mathbf{k}}_\text{i}, \hat{\mathbf{k}}_\text{s})`. 26 27 We assume non-ionized and non-magnetic materials, and therefore the 28 permeability :math:`\mu` of the material is assumed to be equal 29 to the permeability of vacuum i.e., :math:`\mu_r=1.0`. 30 31 For frequency-dependent materials, it is possible to 32 specify a callback function ``frequency_update_callback`` that computes 33 the material properties :math:`(\varepsilon_r, \sigma)` from the 34 frequency. If a callback function is specified, the material properties 35 cannot be set and the values specified at instantiation are ignored. 36 The callback should return `-1` for both the relative permittivity and 37 the conductivity if these are not defined for the given carrier frequency. 38 39 The material properties can be assigned to a TensorFlow variable or 40 tensor. In the latter case, the tensor could be the output of a callable, 41 such as a Keras layer implementing a neural network. In the former case, it 42 could be set to a trainable variable: 43 44 .. code-block:: Python 45 46 mat = RadioMaterial("my_mat") 47 mat.conductivity = tf.Variable(0.0, dtype=tf.float32) 48 49 Parameters 50 ----------- 51 name : str 52 Unique name of the material 53 54 relative_permittivity : float | `None` 55 Relative permittivity of the material. 56 Must be larger or equal to 1. 57 Defaults to 1. Ignored if ``frequency_update_callback`` 58 is provided. 59 60 conductivity : float | `None` 61 Conductivity of the material [S/m]. 62 Must be non-negative. 63 Defaults to 0. 64 Ignored if ``frequency_update_callback`` 65 is provided. 66 67 scattering_coefficient : float 68 Scattering coefficient :math:`S\in[0,1]` as defined in 69 :eq:`scattering_coefficient`. 70 Defaults to 0. 71 72 xpd_coefficient : float 73 Cross-polarization discrimination coefficient :math:`K_x\in[0,1]` as 74 defined in :eq:`xpd`. 75 Only relevant if ``scattering_coefficient``>0. 76 Defaults to 0. 77 78 scattering_pattern : ScatteringPattern 79 :class:`~sionna.rt.ScatteringPattern` to be applied. 80 Only relevant if ``scattering_coefficient``>0. 81 Defaults to `None`, which implies a :class:`~sionna.rt.LambertianPattern`. 82 83 frequency_update_callback : callable | `None` 84 An optional callable object used to obtain the material parameters 85 from the scene's :attr:`~sionna.rt.Scene.frequency`. 86 This callable must take as input the frequency [Hz] and 87 must return the material properties as a tuple: 88 89 ``(relative_permittivity, conductivity)``. 90 91 If set to `None`, the material properties are constant and equal 92 to ``relative_permittivity`` and ``conductivity``. 93 Defaults to `None`. 94 95 dtype : tf.complex64 or tf.complex128 96 Datatype. 97 Defaults to `tf.complex64`. 98 """ 99 100 def __init__(self, 101 name, 102 relative_permittivity=1.0, 103 conductivity=0.0, 104 scattering_coefficient=0.0, 105 xpd_coefficient=0.0, 106 scattering_pattern=None, 107 frequency_update_callback=None, 108 dtype=tf.complex64): 109 110 if not isinstance(name, str): 111 raise TypeError("`name` must be a string") 112 self._name = name 113 114 if dtype not in (tf.complex64, tf.complex128): 115 msg = "`dtype` must be `tf.complex64` or `tf.complex128`" 116 raise ValueError(msg) 117 self._dtype = dtype 118 self._rdtype = dtype.real_dtype 119 120 if scattering_pattern is None: 121 scattering_pattern = LambertianPattern(dtype=dtype) 122 123 self.scattering_pattern = scattering_pattern 124 self.scattering_coefficient = scattering_coefficient 125 self.xpd_coefficient = xpd_coefficient 126 127 if frequency_update_callback is None: 128 self.relative_permittivity = relative_permittivity 129 self.conductivity = conductivity 130 131 # Save the callback for when the frequency is updated 132 # or if the RadioMaterial is added to a scene 133 self._frequency_update_callback = frequency_update_callback 134 135 # When loading a scene, the custom materials (i.e., the materials not 136 # baked-in Sionna but defined by the user) are not defined yet. 137 # If when loading a scene a non-defined material is encountered, 138 # then a "placeholder" material is created which is used until the 139 # material is defined by the user. 140 # Note that propagation simulation cannot be done if placeholders are 141 # used. 142 self._is_placeholder = False # Is this material a placeholder 143 144 # Set of objects identifiers that use this material 145 self._objects_using = set() 146 147 148 @property 149 def name(self): 150 """ 151 str (read-only) : Name of the radio material 152 """ 153 return self._name 154 155 @property 156 def relative_permittivity(self): 157 r""" 158 tf.float : Get/set the relative permittivity 159 :math:`\varepsilon_r` :eq:`eta` 160 """ 161 return self._relative_permittivity 162 163 @relative_permittivity.setter 164 def relative_permittivity(self, v): 165 if isinstance(v, tf.Variable): 166 if v.dtype != self._rdtype: 167 msg = f"`relative_permittivity` must have dtype={self._rdtype}" 168 raise TypeError(msg) 169 else: 170 self._relative_permittivity = v 171 else: 172 self._relative_permittivity = tf.cast(v, self._rdtype) 173 174 @property 175 def relative_permeability(self): 176 r""" 177 tf.float (read-only) : Relative permeability 178 :math:`\mu_r` :eq:`mu`. 179 Defaults to 1. 180 """ 181 return tf.cast(1., self._rdtype) 182 183 @property 184 def conductivity(self): 185 r""" 186 tf.float: Get/set the conductivity 187 :math:`\sigma` [S/m] :eq:`eta` 188 """ 189 return self._conductivity 190 191 @conductivity.setter 192 def conductivity(self, v): 193 if isinstance(v, tf.Variable): 194 if v.dtype != self._rdtype: 195 msg = f"`conductivity` must have dtype={self._rdtype}" 196 raise TypeError(msg) 197 else: 198 self._conductivity = v 199 else: 200 self._conductivity = tf.cast(v, self._rdtype) 201 202 @property 203 def scattering_coefficient(self): 204 r""" 205 tf.float: Get/set the scattering coefficient 206 :math:`S\in[0,1]` :eq:`scattering_coefficient`. 207 """ 208 return self._scattering_coefficient 209 210 @scattering_coefficient.setter 211 def scattering_coefficient(self, v): 212 if isinstance(v, tf.Variable): 213 if v.dtype != self._rdtype: 214 msg=f"`scattering_coefficient` must have dtype={self._rdtype}" 215 raise TypeError(msg) 216 else: 217 self._scattering_coefficient = v 218 else: 219 self._scattering_coefficient = tf.cast(v, self._rdtype) 220 221 @property 222 def xpd_coefficient(self): 223 r""" 224 tf.float: Get/set the cross-polarization discrimination coefficient 225 :math:`K_x\in[0,1]` :eq:`xpd`. 226 """ 227 return self._xpd_coefficient 228 229 @xpd_coefficient.setter 230 def xpd_coefficient(self, v): 231 if isinstance(v, tf.Variable): 232 if v.dtype != self._rdtype: 233 msg=f"`xpd_coefficient` must have dtype={self._rdtype}" 234 raise TypeError(msg) 235 else: 236 self._xpd_coefficient = v 237 else: 238 self._xpd_coefficient = tf.cast(v, self._rdtype) 239 240 @property 241 def scattering_pattern(self): 242 r""" 243 ScatteringPattern: Get/set the ScatteringPattern. 244 """ 245 return self._scattering_pattern 246 247 @scattering_pattern.setter 248 def scattering_pattern(self, v): 249 if not isinstance(v, ScatteringPattern) and v is not None: 250 raise ValueError("Not a valid instanc of ScatteringPattern") 251 self._scattering_pattern = v 252 253 @property 254 def complex_relative_permittivity(self): 255 r""" 256 tf.complex (read-only) : Complex relative permittivity 257 :math:`\eta` :eq:`eta` 258 """ 259 epsilon_0 = DIELECTRIC_PERMITTIVITY_VACUUM 260 eta_prime = self.relative_permittivity 261 sigma = self.conductivity 262 frequency = scene.Scene().frequency 263 omega = tf.cast(2.*PI*frequency, self._rdtype) 264 return tf.complex(eta_prime, 265 -tf.math.divide_no_nan(sigma, epsilon_0*omega)) 266 267 @property 268 def frequency_update_callback(self): 269 """ 270 callable : Get/set frequency update callback function 271 """ 272 return self._frequency_update_callback 273 274 @frequency_update_callback.setter 275 def frequency_update_callback(self, value): 276 self._frequency_update_callback = value 277 self.frequency_update() 278 279 @property 280 def well_defined(self): 281 """bool : Get if the material is well-defined""" 282 # pylint: disable=chained-comparison 283 return ((self._conductivity >= 0.) 284 and (self.relative_permittivity >= 1.) 285 and (0. <= self.scattering_coefficient <= 1.) 286 and (0. <= self.xpd_coefficient <= 1.) 287 and (0. <= self.scattering_pattern.lambda_ <= 1.)) 288 289 @property 290 def use_counter(self): 291 """ 292 int : Number of scene objects using this material 293 """ 294 return len(self._objects_using) 295 296 @property 297 def is_used(self): 298 """bool : Indicator if the material is used by at least one object of 299 the scene""" 300 return self.use_counter > 0 301 302 @property 303 def using_objects(self): 304 """ 305 [num_using_objects], tf.int : Identifiers of the objects using this 306 material 307 """ 308 tf_objects_using = tf.cast(tuple(self._objects_using), tf.int32) 309 return tf_objects_using 310 311 ############################################## 312 # Internal methods. 313 # Should not be documented. 314 ############################################## 315 316 def frequency_update(self): 317 # pylint: disable=line-too-long 318 r"""Callback for when the frequency is updated 319 """ 320 if self._frequency_update_callback is None: 321 return 322 323 parameters = self._frequency_update_callback(scene.Scene().frequency) 324 relative_permittivity, conductivity = parameters 325 self.relative_permittivity = relative_permittivity 326 self.conductivity = conductivity 327 328 def add_object_using(self, object_id): 329 """ 330 Add an object to the set of objects using this material 331 """ 332 self._objects_using.add(object_id) 333 334 def discard_object_using(self, object_id): 335 """ 336 Remove an object from the set of objects using this material 337 """ 338 assert object_id in self._objects_using,\ 339 f"Object with id {object_id} is not in the set of {self.name}" 340 self._objects_using.discard(object_id) 341 342 @property 343 def is_placeholder(self): 344 """ 345 bool : Get/set if this radio material is a placeholder 346 """ 347 return self._is_placeholder 348 349 @is_placeholder.setter 350 def is_placeholder(self, v): 351 self._is_placeholder = v 352 353 def assign(self, rm): 354 """ 355 Assign new values to the radio material properties from another 356 radio material ``rm`` 357 358 Input 359 ------ 360 rm : :class:`~sionna.rt.RadioMaterial 361 Radio material from which to assign the new values 362 """ 363 if not isinstance(rm, RadioMaterial): 364 raise TypeError("`rm` is not a RadioMaterial") 365 self.relative_permittivity = rm.relative_permittivity 366 self.conductivity = rm.conductivity 367 self.scattering_coefficient = rm.scattering_coefficient 368 self.xpd_coefficient = rm.xpd_coefficient 369 self.scattering_pattern = rm.scattering_pattern 370 self.frequency_update_callback = rm.frequency_update_callback