Source code for vlcsim.scene.scenario

"""Scenario class for managing VLC simulation environments."""

import numpy as np
import math as m
import warnings
from typing import List, Optional, Any

from .vled import VLed
from .rf import RF
from .receiver import Receiver


[docs] class Scenario: """Represents the simulation scenario's physical environment and configuration. The Scenario class manages the 3D room dimensions, lighting infrastructure (VLEDs and RF femtocells), receivers, and performs channel gain and capacity calculations for both line-of-sight and wall-reflected signals. Attributes: numberOfAPs: Class variable tracking the total number of access points (VLEDs + RF) created across all scenarios. Properties: - Dimensions: width, length, height, start_x/y, end_x/y - Infrastructure: vleds, rfs, vledsPositions, rfsPositions - Counts: numberOfVLeds, numberOfRFs Example: >>> scenario = Scenario(width=5.0, length=5.0, height=3.0, nGrids=5, rho=0.8) >>> vled = VLed(x=0, y=0, z=3.0, ml=1.0, pt=10.0) >>> scenario.addVLed(vled) >>> scenario.numberOfVLeds 1 """ numberOfAPs = 0 """Class variable tracking the total number of access points created.""" warnings.filterwarnings( "ignore", message="invalid value encountered in double_scalars" )
[docs] def __init__( self, width: float, length: float, height: float, nGrids: int, rho: float ) -> None: """Initialize a simulation scenario with room dimensions and parameters. Args: width: Room width in meters (Y-axis dimension) length: Room length in meters (X-axis dimension) height: Room height in meters (Z-axis dimension) nGrids: Number of grid divisions per meter for discretization rho: Wall reflection coefficient (0.0 to 1.0) Note: The room coordinate system is centered at (0, 0, 0) in the XY plane, with Z extending from 0 (floor) to height (ceiling). """ # Reset all class variables for this new scenario instance # This is essential for proper reinitialization in notebook environments Scenario.numberOfAPs = 0 VLed.numberOfVLeds = 0 RF.numberOfRFs = 0 self.__length: float = length # x self.__width: float = width # y self.__height: float = height # z self.__rho: float = rho self.__start_x: float = -self.__width / 2 self.__start_y: float = -self.__length / 2 self.__end_x: float = self.__width / 2 self.__end_y: float = self.__length / 2 self.__mobile_terminals: List[Receiver] = [] self.__vleds: List[VLed] = [] self.__femtocells: List[RF] = [] self.__vledsPositions: List[int] = [] self.__rfsPositions: List[int] = [] self.__nx = round(self.__length * nGrids) self.__ny = round(self.__width * nGrids) self.__nz = round(self.__height * nGrids) self.__g_x = np.linspace(self.__start_x, self.__end_x, self.__nx) self.__g_y = np.linspace(self.__start_y, self.__end_y, self.__ny) self.__g_z = np.linspace(0, self.__height, self.__nz) self.__g_xyz = np.array([self.__g_x, self.__g_y, self.__g_z], dtype=object)
[docs] def addVLed(self, vled: "VLed") -> None: """Add a VLED access point to the scenario. Args: vled: The VLED device to add """ self.__vleds.append(vled) self.__vledsPositions.append(Scenario.numberOfAPs) Scenario.numberOfAPs += 1
[docs] def addRF(self, rf: "RF") -> None: """Add an RF femtocell to the scenario. Args: rf: The RF femtocell device to add """ self.__femtocells.append(rf) self.__rfsPositions.append(Scenario.numberOfAPs) Scenario.numberOfAPs += 1
[docs] def getPowerInPointFromWalls( self, receiver: "Receiver", vledID: Optional[int] ) -> float: """Calculate the received optical power from wall reflections. Args: receiver: The receiver device at the measurement point vledID: ID of the transmitting VLED, or None Returns: float: Received power from wall reflections in watts Note: Returns 0.0 if vledID is None or out of bounds. """ if vledID is None or vledID < 0 or vledID >= len(self.__vleds): return 0.0 vled = self.__vleds[vledID] h1 = self.__channelGainWall(receiver, vled, 1, 0) h2 = self.__channelGainWall(receiver, vled, 0, 1) h3 = self.__channelGainWall(receiver, vled, 1, 2) h4 = self.__channelGainWall(receiver, vled, 0, 3) power = (h1 + h2 + h3 + h4) * vled.totalPower * receiver.ts * receiver.gCon return float(power)
[docs] def getPowerInPointFromVled( self, receiver: "Receiver", vledID: Optional[int] ) -> float: """Calculate the received optical power via line-of-sight from a VLED. Args: receiver: The receiver device at the measurement point vledID: ID of the transmitting VLED, or None Returns: float: Received LOS power in watts Note: Returns 0.0 if vledID is None, out of bounds, or receiver is outside the FOV of the VLED. """ if vledID is None or vledID < 0 or vledID >= len(self.__vleds): return 0.0 vled = self.__vleds[vledID] D_los = m.sqrt( (receiver.x - vled.x) ** 2 + (receiver.y - vled.y) ** 2 + (receiver.z - vled.z) ** 2 ) cosphi = (vled.z - receiver.z) / D_los # print(vled.x, vled.y, vled.z) # print(receiver.x, receiver.y, receiver.z) # print(cosphi) r_angle = m.degrees(m.acos(cosphi)) H = ( (vled.ml + 1) * receiver.aDet * cosphi ** (vled.ml + 1) / (2 * m.pi * D_los**2) ) power = ( vled.totalPower * H * receiver.ts * receiver.gCon if abs(r_angle) <= receiver.fov else 0 ) return power
def __channelGainWall( self, receiver: Receiver, vled: VLed, posVar: int, posFixed: int ) -> float: """Calculate the channel gain contribution from a specific wall. Args: receiver: The receiver device vled: The transmitting VLED posVar: Index of the varying position coordinate (0=X, 1=Y, 2=Z) posFixed: Index of the fixed wall (0=left X, 1=front Y, 2=right X, 3=back Y) Returns: float: Channel gain from the specified wall (dimensionless) Note: This is a private method used internally by getPowerInPointFromWalls(). """ wall = None dA = self.__height if posFixed == 0: wall = (self.__start_x, 0) dA *= self.__length / (self.__nx * self.__nz) elif posFixed == 1: wall = (self.__start_y, 1) dA *= self.__width / (self.__ny * self.__nz) elif posFixed == 2: wall = (self.__end_x, 0) dA *= self.__length / (self.__nx * self.__nz) elif posFixed == 3: wall = (self.__end_y, 1) dA *= self.__width / (self.__ny * self.__nz) if wall is None: return 0.0 h = 0 wp = np.full(3, wall[0]) g = self.__g_xyz[posVar] # Minimum distance threshold to avoid division by zero # epsilon = 1e-10 meters (0.1 nanometers) - physically impossible to have # a device exactly at a wall point, but mathematically can occur with grid points epsilon = 1e-10 for i in g: wp[posVar] = i for j in self.__g_z: wp[2] = j # D1: Distance from VLed to wall reflection point D1 = m.sqrt(np.dot(vled.position - wp, vled.position - wp)) # Skip if VLed coincides with wall point (D1 ≈ 0 causes division by zero) # Physically: VLed cannot be on the wall, so this point has no contribution if D1 < epsilon: continue cosphi = abs(wp[2] - vled.position[2]) / D1 cosalpha = abs(vled.position[wall[1]] - wp[wall[1]]) / D1 # D2: Distance from wall reflection point to receiver D2 = m.sqrt(np.dot(wp - receiver.position, wp - receiver.position)) # Skip if receiver coincides with wall point (D2 ≈ 0 causes division by zero) # Physically: receiver cannot be on the wall, so this point has no contribution if D2 < epsilon: continue cosbeta = abs(wp[wall[1]] - receiver.position[wall[1]]) / D2 cospsi = abs(wp[2] - receiver.position[2]) / D2 if abs(m.degrees(m.acos(cospsi))) <= receiver.fov: h = h + (vled.ml + 1) * receiver.aDet * self.__rho * dA * ( cosphi**vled.ml ) * cosalpha * cosbeta * cospsi / ( 2 * (m.pi**2) * (D1**2) * (D2**2) ) return h @property def numberOfVLeds(self) -> int: """Get the number of VLEDs in this scenario. Returns: int: Number of VLED access points """ return len(self.__vleds) @property def numberOfRFs(self) -> int: """Get the number of RF femtocells in this scenario. Returns: int: Number of RF access points """ return len(self.__femtocells) @property def vleds(self) -> List[VLed]: """Get the list of VLEDs in this scenario. Returns: List[VLed]: List of all VLED access points """ return self.__vleds @property def rfs(self) -> List[RF]: """Get the list of RF femtocells in this scenario. Returns: List[RF]: List of all RF access points """ return self.__femtocells @property def start_x(self) -> float: """Get the starting X coordinate of the room. Returns: float: Starting X coordinate in meters (left edge) """ return self.__start_x @start_x.setter def start_x(self, value: float) -> None: """Set the starting X coordinate of the room. Args: value: Starting X coordinate in meters """ self.__start_x = value @property def start_y(self) -> float: """Get the starting Y coordinate of the room. Returns: float: Starting Y coordinate in meters (front edge) """ return self.__start_y @start_y.setter def start_y(self, value: float) -> None: """Set the starting Y coordinate of the room. Args: value: Starting Y coordinate in meters """ self.__start_y = value @property def end_x(self) -> float: """Get the ending X coordinate of the room. Returns: float: Ending X coordinate in meters (right edge) """ return self.__end_x @end_x.setter def end_x(self, value: float) -> None: """Set the ending X coordinate of the room. Args: value: Ending X coordinate in meters """ self.__end_x = value @property def end_y(self) -> float: """Get the ending Y coordinate of the room. Returns: float: Ending Y coordinate in meters (back edge) """ return self.__end_y @end_y.setter def end_y(self, value: float) -> None: """Set the ending Y coordinate of the room. Args: value: Ending Y coordinate in meters """ self.__end_y = value @property def height(self) -> float: """Get the scenario room height. Returns: float: Room height in meters (Z-axis dimension) """ return self.__height @height.setter def height(self, value: float) -> None: """Set the scenario room height. Args: value: Room height in meters """ self.__height = value @property def length(self) -> float: """Get the scenario room length. Returns: float: Room length in meters (X-axis dimension) """ return self.__length @length.setter def length(self, value: float) -> None: """Set the scenario room length. Args: value: Room length in meters """ self.__length = value @property def width(self) -> float: """Get the scenario room width. Returns: float: Room width in meters (Y-axis dimension) """ return self.__width @width.setter def width(self, value: float) -> None: """Set the scenario room width. Args: value: Room width in meters """ self.__width = value @property def vledsPositions(self) -> List[int]: """Get the access point indices for VLEDs. Returns: List[int]: List of AP indices corresponding to each VLED """ return self.__vledsPositions @property def rfsPositions(self) -> List[int]: """Get the access point indices for RF femtocells. Returns: List[int]: List of AP indices corresponding to each RF """ return self.__rfsPositions
[docs] def snrVled(self, receiver: Receiver, vled: VLed) -> float: """Calculate the Signal-to-Noise Ratio for a VLC channel. Computes the total SNR at a receiver from a VLED, considering both line-of-sight and wall-reflected signals, including shot noise and thermal noise components. Args: receiver: Receiver device vled: Transmitting VLED Returns: float: SNR (dimensionless), or 0.0 if noise is zero """ vled_id = vled.ID if vled.ID is not None else -1 powerReceived = self.getPowerInPointFromVled( receiver, vled_id ) + self.getPowerInPointFromWalls(receiver, vled_id) rd = (2 * receiver.q * receiver.s * powerReceived * receiver.b) + ( 2 * receiver.q * receiver.ibg * receiver.i1 * receiver.b ) rt = ( 8 * m.pi * receiver.cb * receiver.tk * receiver.n * receiver.a * receiver.b**2 * ( (receiver.i1 / receiver.gv) + (2 * m.pi) * receiver.fr / receiver.gm * receiver.n * receiver.a * receiver.i2 * receiver.b ) ) rg = rd + rt return (receiver.s * powerReceived) ** 2 / rg if rg != 0 else 0.0
[docs] def snrRf(self, receiver: Receiver, rf: RF) -> float: """Calculate the Signal-to-Noise Ratio for an RF channel. Computes the SNR at a receiver from an RF femtocell using the pathloss model. Args: receiver: Receiver device rf: Transmitting RF femtocell Returns: float: SNR (dimensionless) """ df = m.sqrt( (receiver.x - rf.x) ** 2 + (receiver.y - rf.y) ** 2 + (receiver.z - rf.z) ** 2 ) return rf.pf * rf.Af * df ** (-rf.Ef)
[docs] def capacityVled(self, receiver: Receiver, vled: VLed) -> float: """Calculate the channel capacity for a VLC link. Uses Shannon's capacity formula with the SNR from the VLED channel. Args: receiver: Receiver device vled: Transmitting VLED Returns: float: Channel capacity in bits/s """ return vled.B * m.log2(1 + self.snrVled(receiver, vled))
[docs] def capacityRf(self, receiver: Receiver, rf: RF) -> float: """Calculate the channel capacity for an RF link. Uses Shannon's capacity formula with the SNR from the RF channel. Args: receiver: Receiver device rf: Transmitting RF femtocell Returns: float: Channel capacity in bits/s """ return rf.B * m.log2(1 + self.snrRf(receiver, rf))