Source code for pytwinnet.physics.fading
from __future__ import annotations
import math, hashlib, random
from dataclasses import dataclass
from typing import Optional
from .propagation import PropagationModel
from .environment import Environment
from ..core.node import WirelessNode
def _pair_seed(base_seed: int, a: str, b: str, epoch: Optional[int]) -> int:
s = f"{base_seed}|{a}|{b}|{epoch if epoch is not None else 'static'}"
h = hashlib.sha256(s.encode()).hexdigest()
return int(h[:16], 16) # 64-bit
[docs]
@dataclass
class ShadowedModel(PropagationModel):
"""
Wraps a base propagation model and adds *log-normal shadowing* (Gaussian in dB).
Shadowing is deterministic per (tx, rx, epoch) for reproducibility. Change 'epoch'
(int) to refresh the samples (e.g., per-drop or per-time-slot).
"""
base: PropagationModel
sigma_db: float = 6.0
seed: int = 0
epoch: Optional[int] = None
[docs]
def set_epoch(self, epoch: Optional[int]) -> None:
self.epoch = epoch
[docs]
def calculate_path_loss(self, tx: WirelessNode, rx: WirelessNode, environment: Environment) -> float:
pl = self.base.calculate_path_loss(tx, rx, environment)
rng = random.Random(_pair_seed(self.seed, tx.node_id, rx.node_id, self.epoch))
shad = rng.gauss(0.0, self.sigma_db)
return pl + shad
[docs]
@dataclass
class FadedModel(PropagationModel):
"""
Wraps a base model and adds *small-scale fading* (Rayleigh or Rician) as an extra
dB term: PL_faded = PL + fading_loss_db where fading_loss_db = -10*log10(|h|^2).
Fading is deterministic per (tx, rx, epoch). Change 'epoch' to re-sample.
"""
base: PropagationModel
kind: str = "rayleigh" # "rayleigh" or "rician"
K_dB: float = 6.0 # Rician K-factor (dB) if kind == "rician"
seed: int = 0
epoch: Optional[int] = None
[docs]
def set_epoch(self, epoch: Optional[int]) -> None:
self.epoch = epoch
@staticmethod
def _rayleigh_gain(rng: random.Random) -> float:
# |h|^2 ~ Exp(1)
u = max(rng.random(), 1e-12)
return -math.log(u) # exponential(1)
@staticmethod
def _rician_gain(rng: random.Random, K_linear: float) -> float:
# h = s + n, with s = sqrt(K/(K+1)), n ~ CN(0, 1/(K+1))
sigma = math.sqrt(1.0 / (2.0 * (K_linear + 1.0))) # per real/imag
s = math.sqrt(K_linear / (K_linear + 1.0))
xr = rng.gauss(s, sigma)
xi = rng.gauss(0.0, sigma)
return xr * xr + xi * xi # |h|^2
[docs]
def calculate_path_loss(self, tx: WirelessNode, rx: WirelessNode, environment: Environment) -> float:
pl = self.base.calculate_path_loss(tx, rx, environment)
rng = random.Random(_pair_seed(self.seed, tx.node_id, rx.node_id, self.epoch))
if self.kind.lower().startswith("ray"):
g = self._rayleigh_gain(rng)
else:
K_lin = 10 ** (self.K_dB / 10.0)
g = self._rician_gain(rng, K_lin)
# fading loss in dB (negative gain -> positive extra loss)
fading_loss_db = -10.0 * math.log10(max(g, 1e-12))
return pl + fading_loss_db