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