@@ -944,8 +944,9 @@ def free_space_path_loss(freq_mhz, distance_m):
944944 return float (result ) if result .ndim == 0 else result
945945
946946
947- def friis_range_estimate (pt_dbm , pr_dbm , gt_dbi , gr_dbi , freq_mhz ,
948- path_loss_exp = 2.0 , misc_loss_db = 0.0 ):
947+ def friis_range_estimate (
948+ pt_dbm , pr_dbm , gt_dbi , gr_dbi , freq_mhz , path_loss_exp = 2.0 , misc_loss_db = 0.0
949+ ):
949950 """Solve Friis / log-distance model for maximum range.
950951
951952 Allowable path loss:
@@ -972,11 +973,12 @@ def friis_range_estimate(pt_dbm, pr_dbm, gt_dbi, gr_dbi, freq_mhz,
972973 if path_loss_exp <= 0 :
973974 return float ("inf" )
974975 exponent = (pl_max - fspl_d0 ) / (10.0 * path_loss_exp )
975- return 10.0 ** exponent # d0 = 1 m, so d = 10^exponent
976+ return 10.0 ** exponent # d0 = 1 m, so d = 10^exponent
976977
977978
978- def min_tx_gain_for_range (target_range_m , pt_dbm , pr_dbm , gr_dbi ,
979- freq_mhz , path_loss_exp = 2.0 , misc_loss_db = 0.0 ):
979+ def min_tx_gain_for_range (
980+ target_range_m , pt_dbm , pr_dbm , gr_dbi , freq_mhz , path_loss_exp = 2.0 , misc_loss_db = 0.0
981+ ):
980982 """Solve Friis for minimum Tx antenna gain to achieve target range.
981983
982984 Parameters:
@@ -997,8 +999,16 @@ def min_tx_gain_for_range(target_range_m, pt_dbm, pr_dbm, gr_dbi,
997999 return pl_at_range + pr_dbm + misc_loss_db - pt_dbm - gr_dbi
9981000
9991001
1000- def link_margin (pt_dbm , gt_dbi , gr_dbi , freq_mhz , distance_m ,
1001- path_loss_exp = 2.0 , misc_loss_db = 0.0 , pr_sensitivity_dbm = - 98.0 ):
1002+ def link_margin (
1003+ pt_dbm ,
1004+ gt_dbi ,
1005+ gr_dbi ,
1006+ freq_mhz ,
1007+ distance_m ,
1008+ path_loss_exp = 2.0 ,
1009+ misc_loss_db = 0.0 ,
1010+ pr_sensitivity_dbm = - 98.0 ,
1011+ ):
10021012 """Calculate link margin at a given distance.
10031013
10041014 Link margin = Pr_received - Pr_sensitivity
@@ -1023,9 +1033,17 @@ def link_margin(pt_dbm, gt_dbi, gr_dbi, freq_mhz, distance_m,
10231033 return pr_received - pr_sensitivity_dbm
10241034
10251035
1026- def range_vs_azimuth (gain_2d , theta_deg , phi_deg , freq_mhz ,
1027- pt_dbm , pr_dbm , gr_dbi ,
1028- path_loss_exp = 2.0 , misc_loss_db = 0.0 ):
1036+ def range_vs_azimuth (
1037+ gain_2d ,
1038+ theta_deg ,
1039+ phi_deg ,
1040+ freq_mhz ,
1041+ pt_dbm ,
1042+ pr_dbm ,
1043+ gr_dbi ,
1044+ path_loss_exp = 2.0 ,
1045+ misc_loss_db = 0.0 ,
1046+ ):
10291047 """Compute maximum range for each azimuth direction at the horizon.
10301048
10311049 Uses gain at the theta closest to 90° for each phi.
@@ -1049,16 +1067,18 @@ def range_vs_azimuth(gain_2d, theta_deg, phi_deg, freq_mhz,
10491067 theta_90_idx = np .argmin (np .abs (theta_deg - 90.0 ))
10501068 horizon_gain = gain_2d [theta_90_idx , :]
10511069
1052- range_m = np .array ([
1053- friis_range_estimate (pt_dbm , pr_dbm , g , gr_dbi , freq_mhz ,
1054- path_loss_exp , misc_loss_db )
1055- for g in horizon_gain
1056- ])
1070+ range_m = np .array (
1071+ [
1072+ friis_range_estimate (pt_dbm , pr_dbm , g , gr_dbi , freq_mhz , path_loss_exp , misc_loss_db )
1073+ for g in horizon_gain
1074+ ]
1075+ )
10571076 return range_m , horizon_gain
10581077
10591078
10601079# ——— INDOOR / ENVIRONMENTAL PROPAGATION ——————————————————————————
10611080
1081+
10621082def log_distance_path_loss (freq_mhz , distance_m , n = 2.0 , d0 = 1.0 , sigma_db = 0.0 ):
10631083 """Log-distance path loss model with optional shadow fading margin.
10641084
@@ -1115,8 +1135,7 @@ def _itu_get_N(environment, freq_mhz):
11151135 return table [closest ]
11161136
11171137
1118- def itu_indoor_path_loss (freq_mhz , distance_m , n_floors = 0 ,
1119- environment = "office" ):
1138+ def itu_indoor_path_loss (freq_mhz , distance_m , n_floors = 0 , environment = "office" ):
11201139 """ITU-R P.1238 indoor propagation model.
11211140
11221141 PL = 20·log10(f_MHz) + N·log10(d) + Lf(n_floors) - 28
@@ -1170,9 +1189,18 @@ def wall_penetration_loss(freq_mhz, material="drywall"):
11701189 return base_loss * freq_scale
11711190
11721191
1173- def apply_indoor_propagation (gain_2d , theta_deg , phi_deg , freq_mhz ,
1174- pt_dbm , distance_m , n = 3.0 , n_walls = 1 ,
1175- wall_material = "drywall" , sigma_db = 0.0 ):
1192+ def apply_indoor_propagation (
1193+ gain_2d ,
1194+ theta_deg ,
1195+ phi_deg ,
1196+ freq_mhz ,
1197+ pt_dbm ,
1198+ distance_m ,
1199+ n = 3.0 ,
1200+ n_walls = 1 ,
1201+ wall_material = "drywall" ,
1202+ sigma_db = 0.0 ,
1203+ ):
11761204 """Apply indoor propagation model to a measured antenna pattern.
11771205
11781206 Computes received power at a given distance for every (theta, phi) direction:
@@ -1203,6 +1231,7 @@ def apply_indoor_propagation(gain_2d, theta_deg, phi_deg, freq_mhz,
12031231
12041232# ——— MULTIPATH FADING MODELS ———————————————————————————————————
12051233
1234+
12061235def rayleigh_cdf (power_db , mean_power_db = 0.0 ):
12071236 """Rayleigh fading CDF: probability that received power < x.
12081237
@@ -1262,8 +1291,9 @@ def _erf_approx(x):
12621291 sign = np .sign (x )
12631292 x = np .abs (x )
12641293 t = 1.0 / (1.0 + 0.3275911 * x )
1265- poly = t * (0.254829592 + t * (- 0.284496736 + t * (1.421413741
1266- + t * (- 1.453152027 + t * 1.061405429 ))))
1294+ poly = t * (
1295+ 0.254829592 + t * (- 0.284496736 + t * (1.421413741 + t * (- 1.453152027 + t * 1.061405429 )))
1296+ )
12671297 result = 1.0 - poly * np .exp (- x * x )
12681298 return sign * result
12691299
@@ -1322,8 +1352,9 @@ def _norm_ppf_approx(p):
13221352 return - (t - (c0 + c1 * t + c2 * t ** 2 ) / (1.0 + d1 * t + d2 * t ** 2 + d3 * t ** 3 ))
13231353
13241354
1325- def apply_statistical_fading (gain_2d , theta_deg , phi_deg ,
1326- fading = "rayleigh" , K = 10 , realizations = 1000 ):
1355+ def apply_statistical_fading (
1356+ gain_2d , theta_deg , phi_deg , fading = "rayleigh" , K = 10 , realizations = 1000
1357+ ):
13271358 """Apply statistical fading to a measured pattern via Monte-Carlo.
13281359
13291360 For each (theta, phi) direction, generates `realizations` fading
@@ -1352,8 +1383,10 @@ def apply_statistical_fading(gain_2d, theta_deg, phi_deg,
13521383 # Rician: |h|^2 where h = sqrt(K/(K+1)) + sqrt(1/(K+1))·CN(0,1)
13531384 mu = np .sqrt (K / (K + 1.0 ))
13541385 sigma = np .sqrt (1.0 / (2.0 * (K + 1.0 )))
1355- h = mu + sigma * (np .random .randn (realizations , n_theta , n_phi )
1356- + 1j * np .random .randn (realizations , n_theta , n_phi ))
1386+ h = mu + sigma * (
1387+ np .random .randn (realizations , n_theta , n_phi )
1388+ + 1j * np .random .randn (realizations , n_theta , n_phi )
1389+ )
13571390 h_sq = np .abs (h ) ** 2
13581391
13591392 # Faded power: gain_linear × fading_coefficient
@@ -1397,8 +1430,8 @@ def delay_spread_estimate(distance_m, environment="indoor"):
13971430
13981431# ——— ENHANCED MIMO ANALYSIS ——————————————————————————————————————
13991432
1400- def envelope_correlation_from_patterns ( E1_theta , E1_phi , E2_theta , E2_phi ,
1401- theta_deg , phi_deg ):
1433+
1434+ def envelope_correlation_from_patterns ( E1_theta , E1_phi , E2_theta , E2_phi , theta_deg , phi_deg ):
14021435 """Compute Envelope Correlation Coefficient from 3D far-field patterns.
14031436
14041437 IEEE definition:
@@ -1420,17 +1453,17 @@ def envelope_correlation_from_patterns(E1_theta, E1_phi, E2_theta, E2_phi,
14201453 sin_theta = np .sin (theta_rad )
14211454
14221455 # Cross-correlation integral
1423- integrand_cross = ( E1_theta * np .conj (E2_theta ) + E1_phi * np .conj (E2_phi ) )
1456+ integrand_cross = E1_theta * np .conj (E2_theta ) + E1_phi * np .conj (E2_phi )
14241457 cross = np .sum (integrand_cross * sin_theta [:, np .newaxis ])
14251458
14261459 # Self-correlation integrals
1427- self1 = np .sum ((np .abs (E1_theta )** 2 + np .abs (E1_phi )** 2 ) * sin_theta [:, np .newaxis ])
1428- self2 = np .sum ((np .abs (E2_theta )** 2 + np .abs (E2_phi )** 2 ) * sin_theta [:, np .newaxis ])
1460+ self1 = np .sum ((np .abs (E1_theta ) ** 2 + np .abs (E1_phi ) ** 2 ) * sin_theta [:, np .newaxis ])
1461+ self2 = np .sum ((np .abs (E2_theta ) ** 2 + np .abs (E2_phi ) ** 2 ) * sin_theta [:, np .newaxis ])
14291462
14301463 denom = self1 * self2
14311464 if denom == 0 :
14321465 return 1.0 # Degenerate case
1433- return float (np .abs (cross )** 2 / denom )
1466+ return float (np .abs (cross ) ** 2 / denom )
14341467
14351468
14361469def combining_gain (gains_db , method = "mrc" ):
@@ -1454,7 +1487,7 @@ def combining_gain(gains_db, method="mrc"):
14541487 combined_lin = np .sum (gains_lin )
14551488 elif method == "egc" :
14561489 # EGC: (sum of amplitudes)^2 / N
1457- combined_lin = (np .sum (np .sqrt (gains_lin )))** 2 / len (gains_lin )
1490+ combined_lin = (np .sum (np .sqrt (gains_lin ))) ** 2 / len (gains_lin )
14581491 elif method == "sc" :
14591492 # SC: select the best branch
14601493 combined_lin = best_single
@@ -1466,8 +1499,7 @@ def combining_gain(gains_db, method="mrc"):
14661499 return combined_db , improvement_db
14671500
14681501
1469- def mimo_capacity_vs_snr (ecc , snr_range_db = (- 5 , 30 ), num_points = 36 ,
1470- fading = "rayleigh" , K = 10 ):
1502+ def mimo_capacity_vs_snr (ecc , snr_range_db = (- 5 , 30 ), num_points = 36 , fading = "rayleigh" , K = 10 ):
14711503 """Compute MIMO capacity curves over an SNR range.
14721504
14731505 Returns capacity for SISO, 2×2 AWGN, and 2×2 fading channels.
@@ -1489,10 +1521,9 @@ def mimo_capacity_vs_snr(ecc, snr_range_db=(-5, 30), num_points=36,
14891521 snr_axis = np .linspace (snr_range_db [0 ], snr_range_db [1 ], num_points )
14901522 siso_cap = np .log2 (1 + 10 ** (snr_axis / 10.0 ))
14911523 awgn_cap = np .array ([capacity_awgn (ecc , s ) for s in snr_axis ])
1492- fading_cap = np .array ([
1493- capacity_monte_carlo (ecc , s , fading = fading , K = K , trials = 500 )
1494- for s in snr_axis
1495- ])
1524+ fading_cap = np .array (
1525+ [capacity_monte_carlo (ecc , s , fading = fading , K = K , trials = 500 ) for s in snr_axis ]
1526+ )
14961527 return snr_axis , siso_cap , awgn_cap , fading_cap
14971528
14981529
@@ -1549,8 +1580,7 @@ def mean_effective_gain_mimo(gain_2d_list, theta_deg, phi_deg, xpr_db=6.0):
15491580}
15501581
15511582
1552- def body_worn_pattern_analysis (gain_2d , theta_deg , phi_deg , freq_mhz ,
1553- body_positions = None ):
1583+ def body_worn_pattern_analysis (gain_2d , theta_deg , phi_deg , freq_mhz , body_positions = None ):
15541584 """Analyze antenna pattern across multiple body-worn positions.
15551585
15561586 For each position, applies the directional human shadow model and
@@ -1616,8 +1646,9 @@ def body_worn_pattern_analysis(gain_2d, theta_deg, phi_deg, freq_mhz,
16161646 return results
16171647
16181648
1619- def dense_device_interference (num_devices , tx_power_dbm , freq_mhz ,
1620- bandwidth_mhz = 2.0 , room_size_m = (10 , 10 , 3 )):
1649+ def dense_device_interference (
1650+ num_devices , tx_power_dbm , freq_mhz , bandwidth_mhz = 2.0 , room_size_m = (10 , 10 , 3 )
1651+ ):
16211652 """Estimate aggregate interference in a dense device deployment.
16221653
16231654 Monte-Carlo: places N co-channel devices at random positions in a room
@@ -1644,11 +1675,13 @@ def dense_device_interference(num_devices, tx_power_dbm, freq_mhz,
16441675 sinr_values = []
16451676 for _ in range (n_trials ):
16461677 # Random device positions
1647- positions = np .column_stack ([
1648- np .random .uniform (0 , lx , num_devices ),
1649- np .random .uniform (0 , ly , num_devices ),
1650- np .random .uniform (0 , lz , num_devices ),
1651- ])
1678+ positions = np .column_stack (
1679+ [
1680+ np .random .uniform (0 , lx , num_devices ),
1681+ np .random .uniform (0 , ly , num_devices ),
1682+ np .random .uniform (0 , lz , num_devices ),
1683+ ]
1684+ )
16521685 # Receiver at room center
16531686 rx = np .array ([lx / 2 , ly / 2 , lz / 2 ])
16541687 distances = np .linalg .norm (positions - rx , axis = 1 )
@@ -1673,8 +1706,9 @@ def dense_device_interference(num_devices, tx_power_dbm, freq_mhz,
16731706 return float (np .mean (sinr_distribution )), sinr_distribution , noise_floor_dbm
16741707
16751708
1676- def sar_exposure_estimate (tx_power_mw , antenna_gain_dbi , distance_cm ,
1677- freq_mhz , tissue_type = "muscle" ):
1709+ def sar_exposure_estimate (
1710+ tx_power_mw , antenna_gain_dbi , distance_cm , freq_mhz , tissue_type = "muscle"
1711+ ):
16781712 """Simplified SAR estimation for regulatory screening.
16791713
16801714 Uses far-field power density and tissue absorption:
@@ -1711,7 +1745,7 @@ def sar_exposure_estimate(tx_power_mw, antenna_gain_dbi, distance_cm,
17111745 gt_lin = 10 ** (antenna_gain_dbi / 10.0 )
17121746 pt_w = tx_power_mw / 1000.0
17131747 d_m = max (distance_cm / 100.0 , 0.001 )
1714- S = (pt_w * gt_lin ) / (4 * np .pi * d_m ** 2 ) # W/m²
1748+ S = (pt_w * gt_lin ) / (4 * np .pi * d_m ** 2 ) # W/m²
17151749
17161750 # SAR ≈ σ · E² / ρ, and S = E²/(2·η), so SAR ≈ 2·σ·S/ρ
17171751 # This is the surface SAR, averaged over penetration depth
@@ -1724,8 +1758,7 @@ def sar_exposure_estimate(tx_power_mw, antenna_gain_dbi, distance_cm,
17241758 return sar , fcc_limit , icnirp_limit , bool (sar < fcc_limit and sar < icnirp_limit )
17251759
17261760
1727- def wban_link_budget (tx_power_dbm , freq_mhz , body_channel = "on_body" ,
1728- distance_cm = 30 ):
1761+ def wban_link_budget (tx_power_dbm , freq_mhz , body_channel = "on_body" , distance_cm = 30 ):
17291762 """IEEE 802.15.6 WBAN channel model link budget.
17301763
17311764 Simplified path loss models from IEEE 802.15.6 standard.
0 commit comments