Source code for sionna.phy.config
#
# SPDX-FileCopyrightText: Copyright (c) 2021-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
"""Global Sionna PHY Configuration"""
from __future__ import annotations
import random
from typing import Literal
import numpy as np
import torch
__all__ = ["Config", "config", "dtypes", "Precision"]
# Type aliases
Precision = Literal["single", "double"]
# Mapping from precision to dtypes
dtypes: dict[str, dict[str, dict[str, type]]] = {
"single": {
"torch": {"cdtype": torch.complex64, "dtype": torch.float32},
"np": {"cdtype": np.complex64, "dtype": np.float32},
},
"double": {
"torch": {"cdtype": torch.complex128, "dtype": torch.float64},
"np": {"cdtype": np.complex128, "dtype": np.float64},
},
}
[docs]
class Config:
"""Sionna PHY Configuration Class
This singleton class is used to define global configuration variables
and random number generators that can be accessed from all modules
and functions. It is instantiated immediately and its properties can be
accessed as :code:`sionna.phy.config.desired_property`.
"""
_instance: Config | None = None
def __new__(cls) -> Config:
"""Create or return the singleton Config instance."""
if cls._instance is None:
instance = object.__new__(cls)
cls._instance = instance
cls._instance._initialized = False
return cls._instance
def __init__(self) -> None:
"""Initialize the Config instance with default values."""
if self._initialized:
return
self._initialized: bool = True
# Initialize private properties
self._precision: Precision | None = None
self._device: str | None = None
self._seed: int | None = None
self._py_rng: random.Random | None = None
self._np_rng: np.random.Generator | None = None
self._torch_rngs: dict[str, torch.Generator] = {}
self._np_dtype: type | None = None
self._np_cdtype: type | None = None
self._dtype: torch.dtype | None = None
self._cdtype: torch.dtype | None = None
# Set default property values
self.precision = "single"
self.device = None
self.seed = None
@property
def py_rng(self) -> random.Random:
"""`random.Random` : Python random number generator
Example
-------
.. code-block:: python
from sionna.phy import config
config.seed = 42 # Set seed for deterministic results
# Use generator instead of random
val = config.py_rng.randint(0, 10)
"""
if self._py_rng is None:
self._py_rng = random.Random(self.seed)
return self._py_rng
@property
def np_rng(self) -> np.random.Generator:
"""`np.random.Generator` : NumPy random number generator
Example
-------
.. code-block:: python
from sionna.phy import config
config.seed = 42 # Set seed for deterministic results
# Use generator instead of np.random
noise = config.np_rng.normal(size=[4])
"""
if self._np_rng is None:
self._np_rng = np.random.default_rng(self.seed)
return self._np_rng
[docs]
def torch_rng(self, device: str | None = None) -> torch.Generator:
"""`torch.Generator` : PyTorch random number generator for the specified device
:param device: Device name (e.g., ``'cpu'``, ``'cuda:0'``).
If `None`, :attr:`~sionna.phy.config.Config.device` is used.
Example
-------
.. code-block:: python
from sionna.phy import config
config.seed = 42 # Set seed for deterministic results
# Use generator instead of torch.randn
noise = torch.randn([4], generator=config.torch_rng())
"""
if device is None:
device = self.device
return self._torch_rngs[device]
def _reset_rngs(self) -> None:
"""Reset all random number generators."""
# Uses device-specific seed offsets (seed + device_index) to ensure
# different devices produce different random streams. This applies to
# both the explicit generators (used in eager mode) and the default
# CUDA generators (used in compiled mode via global RNG).
self._py_rng = None
self._np_rng = None
self._torch_rngs = {}
# Initialize CUDA to populate default_generators if available
if torch.cuda.is_available() and torch.cuda.device_count() > 0:
torch.cuda.init()
for i, device in enumerate(self.available_devices):
self._torch_rngs[device] = torch.Generator(device=device)
if self._seed is None:
# Random seeding - each device gets a random seed
self._torch_rngs[device].seed()
# Also seed default generators randomly
if device == "cpu":
torch.default_generator.seed()
elif device.startswith("cuda:"):
device_idx = int(device.split(":")[1])
# Check if default generators are initialized and index is valid
if torch.cuda.is_available() and device_idx < len(
torch.cuda.default_generators
):
torch.cuda.default_generators[device_idx].seed()
else:
# Deterministic seeding with device-specific offset
# This ensures different devices produce different streams
device_seed = self._seed + i
self._torch_rngs[device].manual_seed(device_seed)
# Also seed default generators for compiled mode
if device == "cpu":
torch.default_generator.manual_seed(device_seed)
elif device.startswith("cuda:"):
device_idx = int(device.split(":")[1])
# Check if default generators are initialized and index is valid
if torch.cuda.is_available() and device_idx < len(
torch.cuda.default_generators
):
torch.cuda.default_generators[device_idx].manual_seed(
device_seed
)
@property
def seed(self) -> int | None:
"""`None` (default) | `int` : Get/set seed for all random number generators
All random number generators used internally by Sionna
can be configured with a common seed to ensure reproducibility
of results. It defaults to `None` which implies that a random
seed will be used and results are non-deterministic.
Example
-------
.. code-block:: python
# This code will lead to deterministic results
from sionna.phy import config
from sionna.phy.mapping import BinarySource
config.seed = 42
print(BinarySource()([10]))
"""
return self._seed
@seed.setter
def seed(self, seed: int | None) -> None:
self._seed = seed
self._reset_rngs()
@property
def device(self) -> str:
"""`str` : Get/set the device for computation (e.g., ``'cpu'``, ``'cuda:0'``)"""
return self._device
@device.setter
def device(self, v: str | None) -> None:
# Set default device if None
if v is None:
if torch.cuda.is_available() and torch.cuda.device_count() > 0:
v = "cuda:0"
else:
v = "cpu"
# If device is already set to the desired value, do nothing
if self._device == v:
return
# Raise error if device is invalid
if v not in self.available_devices:
raise ValueError(f"Invalid device: {v}")
# Set device value
self._device = v
@property
def np_dtype(self) -> np.dtype:
"""`np.dtype` : Default NumPy dtype for real floating point numbers"""
return dtypes[self.precision]["np"]["dtype"]
@property
def np_cdtype(self) -> np.dtype:
"""`np.dtype` : Default NumPy dtype for complex floating point numbers"""
return dtypes[self.precision]["np"]["cdtype"]
@property
def dtype(self) -> torch.dtype:
"""`torch.dtype` : Default PyTorch dtype for real floating point numbers"""
return dtypes[self.precision]["torch"]["dtype"]
@property
def cdtype(self) -> torch.dtype:
"""`torch.dtype` : Default PyTorch dtype for complex floating point numbers"""
return dtypes[self.precision]["torch"]["cdtype"]
@property
def precision(self) -> Precision:
"""``"single"`` (default) | ``"double"`` : Default precision used for all computations
The ``"single"`` option represents real-valued floating-point numbers
using 32 bits, whereas the ``"double"`` option uses 64 bits.
For complex-valued data types, each component of the complex number
(real and imaginary parts) uses either 32 bits (for ``"single"``)
or 64 bits (for ``"double"``).
"""
return self._precision
@precision.setter
def precision(self, v: Precision) -> None:
if v not in dtypes:
raise ValueError("Precision must be 'single' or 'double'.")
self._precision = v
@property
def available_devices(self) -> list[str]:
"""`list` of `str` : List of available compute devices"""
devices = ["cpu"]
if torch.cuda.is_available():
for i in range(torch.cuda.device_count()):
devices.append(f"cuda:{i}")
return devices
config = Config()