Skip to content

Commit 9512447

Browse files
RFingAdamclaude
andcommitted
style: Run black formatter, remove unused imports
- black reformatted 7 files (plotting, calculations, file_utils, config_template, dialogs_mixin, callbacks_mixin, tools_mixin) - Removed unused imports: apply_indoor_propagation, PROTOCOL_PRESETS, ENVIRONMENT_PRESETS, Axes3D from plotting.py - All CI checks pass: 0 flake8 critical errors, black clean, 300 tests pass Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent a561876 commit 9512447

7 files changed

Lines changed: 658 additions & 384 deletions

File tree

plot_antenna/calculations.py

Lines changed: 86 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
10621082
def 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+
12061235
def 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

14361469
def 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.

plot_antenna/config_template.py

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -251,12 +251,12 @@
251251
LINK_BUDGET_PROTOCOL_PRESET = "BLE 1Mbps" # "Custom", "BLE 1Mbps", "BLE 2Mbps",
252252
# "BLE Long Range (Coded)", "WiFi 802.11n (MCS0)", "WiFi 802.11ac (MCS0)",
253253
# "Zigbee / Thread", "LoRa SF12", "LoRa SF7", "LTE Cat-M1", "NB-IoT"
254-
LINK_BUDGET_TX_POWER_DBM = 0.0 # Transmit power in dBm
255-
LINK_BUDGET_RX_SENSITIVITY_DBM = -98.0 # Receiver sensitivity in dBm
256-
LINK_BUDGET_RX_GAIN_DBI = 0.0 # Receive antenna gain in dBi
257-
LINK_BUDGET_PATH_LOSS_EXP = 2.0 # Path loss exponent (2=free-space, 3=indoor, 4=worst)
258-
LINK_BUDGET_MISC_LOSS_DB = 10.0 # Cable, mismatch, body loss, etc. in dB
259-
LINK_BUDGET_TARGET_RANGE_M = 5.0 # Target range for reverse calculations in metres
254+
LINK_BUDGET_TX_POWER_DBM = 0.0 # Transmit power in dBm
255+
LINK_BUDGET_RX_SENSITIVITY_DBM = -98.0 # Receiver sensitivity in dBm
256+
LINK_BUDGET_RX_GAIN_DBI = 0.0 # Receive antenna gain in dBi
257+
LINK_BUDGET_PATH_LOSS_EXP = 2.0 # Path loss exponent (2=free-space, 3=indoor, 4=worst)
258+
LINK_BUDGET_MISC_LOSS_DB = 10.0 # Cable, mismatch, body loss, etc. in dB
259+
LINK_BUDGET_TARGET_RANGE_M = 5.0 # Target range for reverse calculations in metres
260260

261261
# ============================================================================
262262
# INDOOR PROPAGATION ANALYSIS
@@ -265,13 +265,13 @@
265265
# including wall penetration (ITU-R P.2040) and shadow fading.
266266

267267
INDOOR_ANALYSIS_ENABLED = False
268-
INDOOR_ENVIRONMENT = "Office" # "Free Space", "Office", "Residential", "Commercial",
268+
INDOOR_ENVIRONMENT = "Office" # "Free Space", "Office", "Residential", "Commercial",
269269
# "Hospital", "Industrial", "Outdoor Urban", "Outdoor LOS"
270-
INDOOR_PATH_LOSS_EXP = 3.0 # Overridable, auto-set by environment preset
271-
INDOOR_NUM_WALLS = 1 # Number of wall obstructions
272-
INDOOR_WALL_MATERIAL = "drywall" # drywall, wood, glass, brick, concrete, metal
273-
INDOOR_SHADOW_FADING_DB = 5.0 # Log-normal shadow fading std deviation (dB)
274-
INDOOR_MAX_DISTANCE_M = 30.0 # Max range for coverage plots (metres)
270+
INDOOR_PATH_LOSS_EXP = 3.0 # Overridable, auto-set by environment preset
271+
INDOOR_NUM_WALLS = 1 # Number of wall obstructions
272+
INDOOR_WALL_MATERIAL = "drywall" # drywall, wood, glass, brick, concrete, metal
273+
INDOOR_SHADOW_FADING_DB = 5.0 # Log-normal shadow fading std deviation (dB)
274+
INDOOR_MAX_DISTANCE_M = 30.0 # Max range for coverage plots (metres)
275275

276276
# ============================================================================
277277
# MULTIPATH FADING ANALYSIS
@@ -280,10 +280,10 @@
280280
# performance in multipath environments.
281281

282282
FADING_ANALYSIS_ENABLED = False
283-
FADING_MODEL = "rayleigh" # "rayleigh" (NLOS) or "rician" (LOS)
284-
FADING_RICIAN_K = 10 # Rician K-factor (linear, LOS/scattered ratio)
285-
FADING_TARGET_RELIABILITY = 99.0 # Target reliability percentage for fade margin
286-
FADING_REALIZATIONS = 1000 # Monte-Carlo fading realizations
283+
FADING_MODEL = "rayleigh" # "rayleigh" (NLOS) or "rician" (LOS)
284+
FADING_RICIAN_K = 10 # Rician K-factor (linear, LOS/scattered ratio)
285+
FADING_TARGET_RELIABILITY = 99.0 # Target reliability percentage for fade margin
286+
FADING_REALIZATIONS = 1000 # Monte-Carlo fading realizations
287287

288288
# ============================================================================
289289
# MIMO / DIVERSITY ANALYSIS
@@ -292,11 +292,11 @@
292292
# and pattern-based metrics. Requires ECC analysis to be enabled.
293293

294294
MIMO_ANALYSIS_ENABLED = False
295-
MIMO_SNR_DB = 20 # Operating SNR for capacity analysis (dB)
296-
MIMO_SNR_RANGE_DB = (-5, 30) # SNR range for capacity-vs-SNR curves
297-
MIMO_FADING_MODEL = "rayleigh" # "rayleigh" or "rician" for capacity simulation
298-
MIMO_RICIAN_K = 10 # K-factor for Rician MIMO channels
299-
MIMO_XPR_DB = 6.0 # Cross-polarization ratio (indoor ~6 dB)
295+
MIMO_SNR_DB = 20 # Operating SNR for capacity analysis (dB)
296+
MIMO_SNR_RANGE_DB = (-5, 30) # SNR range for capacity-vs-SNR curves
297+
MIMO_FADING_MODEL = "rayleigh" # "rayleigh" or "rician" for capacity simulation
298+
MIMO_RICIAN_K = 10 # K-factor for Rician MIMO channels
299+
MIMO_XPR_DB = 6.0 # Cross-polarization ratio (indoor ~6 dB)
300300

301301
# ============================================================================
302302
# WEARABLE / MEDICAL DEVICE ANALYSIS
@@ -306,6 +306,6 @@
306306

307307
WEARABLE_ANALYSIS_ENABLED = False
308308
WEARABLE_BODY_POSITIONS = ["wrist", "chest", "hip", "head"]
309-
WEARABLE_TX_POWER_MW = 1.0 # Transmit power in milliwatts
310-
WEARABLE_DENSE_DEVICE_COUNT = 20 # Number of nearby co-channel devices
311-
WEARABLE_ROOM_SIZE_M = (10, 10, 3) # Room dimensions (L, W, H) in metres
309+
WEARABLE_TX_POWER_MW = 1.0 # Transmit power in milliwatts
310+
WEARABLE_DENSE_DEVICE_COUNT = 20 # Number of nearby co-channel devices
311+
WEARABLE_ROOM_SIZE_M = (10, 10, 3) # Room dimensions (L, W, H) in metres

0 commit comments

Comments
 (0)