Skip to content

Commit

Permalink
Merge pull request #41 from RFingAdam/dev
Browse files Browse the repository at this point in the history
Release v3.0.0 - See Release Notes
  • Loading branch information
RFingAdam authored Oct 23, 2024
2 parents 3f4eb85 + 316c76c commit ce218c2
Show file tree
Hide file tree
Showing 10 changed files with 1,091 additions and 297 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,6 @@ debug/

# Icon files
*.ico

#OpenApi Key
openapi.env
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# RFlect - Antenna Plot Tool <img src="./assets/smith_logo.png" alt="RFlect Logo" width="40">


**Version:** 2.2.2
**Version:** 3.0.0

RFlect is a comprehensive antenna plotting tool, currently designed specifically for visualizing and analyzing antenna measurements from the Howland Company 3100 Antenna Chamber and WTL Test Lab outputs. Additionally, it offers support for .csv VNA files from Copper Mountain RVNA and S2VNA software of S11/VSWR/Group Delay(S21(s)) measurements, making it a versatile choice for a wide range of antenna data processing needs. Through its user-friendly graphical interface, RFlect provides an intuitive way to handle various antenna metrics and visualize results.

Expand Down
12 changes: 12 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
# RFlect - Release Notes

## Version 3.0.0 (10/23/2024)
- Implemented Active 3D plotting
- Implement Save Results to File for Active Scans
- Added Setting for Active & __TODO__ Passive 3D plotting to use a interpolated meshing
- Added Console in the GUI for Status Updates/Errors/Warnings/Messages
- Added Support for Report Generation to Word Documents via save.py
- Implement Report Generation for an entire selected folder to capture all tested frequencies and results
- Impelent Report Generation for both Active and Passive Scans
- Added Support for OpenAI API for Image Captioning for report generation
- Create openapi.env file for API Key for option to appear .gitignore added. Source only
- __TODO__ Added placeholder for NF2FF conversion for Passive measurements in the nearfield for investigation for low frequency measurements

## Version 2.2.2 (07/31/2024)
- Mirror Active & Passive 2D plots to appropriate conventions
- Added Labeling for 2D plots to show the Phi angle on the plot.
Expand Down
273 changes: 203 additions & 70 deletions plot_antenna/calculations.py
Original file line number Diff line number Diff line change
@@ -1,95 +1,87 @@
# calculations.py

import numpy as np
from scipy.constants import c # Speed of light
from scipy.signal import windows

# TODO _____________Active Calculation Functions___________
def calculate_active_variables(start_phi, stop_phi, start_theta, stop_theta,
inc_phi, inc_theta, h_power_dBm, v_power_dBm):
"""
Calculate variables for TRP/Active Measurement Plotting.
Parameters:
- [all the input parameters]: Relevant data needed for calculations.
Returns:
- tuple: All required calculated variables.
"""
# TODO Calculate the Required Variables for Active/TRP plotting
theta_points = (stop_theta - start_theta) + 1
phi_points = (stop_phi - start_phi) + 1
data_points = (theta_points)*(phi_points)

theta_angles_rad = np.deg2rad(np.arange(start_theta, stop_theta + inc_theta, inc_theta))
phi_angles_rad = np.deg2rad(np.arange(start_phi, stop_phi + inc_phi, inc_phi))

# Power calculations
total_power_dBm = 10 * np.log10(10**(v_power_dBm/10) + 10**(h_power_dBm/10))
total_power_dBm_min = np.min(total_power_dBm)
total_power_dBm_nom = total_power_dBm - total_power_dBm_min

h_power_dBm_min = np.min(h_power_dBm)
h_power_dBm_nom = h_power_dBm - h_power_dBm_min

v_power_dBm_min = np.min(v_power_dBm)
v_power_dBm_nom = v_power_dBm - v_power_dBm_min

# Reshape data into 2D arrays for calculations
unique_theta = np.sort(np.arange(start_theta, stop_theta + inc_theta, inc_theta))
unique_phi = np.sort(np.arange(start_phi, stop_phi + inc_phi, inc_phi))

# Reshaping them into 2D arrays for easier calculations
h_power_dBm_2d = h_power_dBm.reshape((len(unique_theta), len(unique_phi)))
v_power_dBm_2d = v_power_dBm.reshape((len(unique_theta), len(unique_phi)))
total_power_dBm_2d = 10 * np.log10(10**(h_power_dBm_2d/10) + 10**(v_power_dBm_2d/10))

# TRP calculations using reshaped data
TRP_dBm = 10 * np.log10(np.sum(10**(h_power_dBm_2d/10 + v_power_dBm_2d/10) *
np.sin(theta_angles_rad)[:, None] * np.pi/2) /
(theta_points * phi_points))
h_TRP_dBm = 10 * np.log10(np.sum(10**(h_power_dBm_2d/10) *
np.sin(theta_angles_rad)[:, None] * np.pi/2) /
theta_points = int((stop_theta - start_theta) / inc_theta + 1)
phi_points = int((stop_phi - start_phi) / inc_phi + 1)
data_points = theta_points * phi_points

# Calculate theta and phi angles in degrees
theta_angles_deg = np.linspace(start_theta, stop_theta, theta_points)
phi_angles_deg = np.linspace(start_phi, stop_phi, phi_points)

# Reshape data into 2D arrays
h_power_dBm_2d = h_power_dBm.reshape((theta_points, phi_points))
v_power_dBm_2d = v_power_dBm.reshape((theta_points, phi_points))

# Append 360 degrees if not already included
if phi_angles_deg[-1] < 360:
phi_angles_deg = np.append(phi_angles_deg, 360)
# Append first column of data to the end
h_power_dBm_2d = np.hstack((h_power_dBm_2d, h_power_dBm_2d[:, [0]]))
v_power_dBm_2d = np.hstack((v_power_dBm_2d, v_power_dBm_2d[:, [0]]))
phi_points += 1 # Increase the phi_points

# Convert angles to radians
theta_angles_rad = np.deg2rad(theta_angles_deg)
phi_angles_rad = np.deg2rad(phi_angles_deg)

# Recalculate total power in dBm
total_power_dBm_2d = 10 * np.log10(10**(v_power_dBm_2d / 10) + 10**(h_power_dBm_2d / 10))

# Compute minimum and nominal power values if needed
total_power_dBm_min = np.min(total_power_dBm_2d)
total_power_dBm_nom = total_power_dBm_2d - total_power_dBm_min

h_power_dBm_min = np.min(h_power_dBm_2d)
h_power_dBm_nom = h_power_dBm_2d - h_power_dBm_min

v_power_dBm_min = np.min(v_power_dBm_2d)
v_power_dBm_nom = v_power_dBm_2d - v_power_dBm_min

# Compute TRP_dBm, h_TRP_dBm, v_TRP_dBm
TRP_dBm = 10 * np.log10(np.sum(10**(total_power_dBm_2d / 10) *
np.sin(theta_angles_rad)[:, None] * np.pi / 2) /
(theta_points * phi_points))
h_TRP_dBm = 10 * np.log10(np.sum(10**(h_power_dBm_2d / 10) *
np.sin(theta_angles_rad)[:, None] * np.pi / 2) /
(theta_points * phi_points))
v_TRP_dBm = 10 * np.log10(np.sum(10**(v_power_dBm_2d/10) *
np.sin(theta_angles_rad)[:, None] * np.pi/2) /
v_TRP_dBm = 10 * np.log10(np.sum(10**(v_power_dBm_2d / 10) *
np.sin(theta_angles_rad)[:, None] * np.pi / 2) /
(theta_points * phi_points))

'''# TODO DEBUG Adding Print Statements for Debugging
print(f"Data Points: {data_points}")
print(f"Theta Angles (Rad): {theta_angles_rad[:5]}")
print(f"Phi Angles (Rad): {phi_angles_rad[:5]}")
print(f"Sample Total Power (dBm 2D): {total_power_dBm_2d[:5, :5]}")
print(f"Total Power Min (dBm): {total_power_dBm_min}")
print(f"Sample Total Power Nominal (dBm): {total_power_dBm_nom[:5]}")
print(f"Sample H Power (dBm 2D): {h_power_dBm_2d[:5, :5]}")
print(f"H Power Min (dBm): {h_power_dBm_min}")
print(f"Sample V Power (dBm 2D): {v_power_dBm_2d[:5, :5]}")
print(f"V Power Min (dBm): {v_power_dBm_min}")
print(f"Sample H Power Nominal (dBm): {h_power_dBm_nom[:5]}")
print(f"Sample V Power Nominal (dBm): {v_power_dBm_nom[:5]}")
print(f"TRP (dBm): {TRP_dBm}")
print(f"H TRP (dBm): {h_TRP_dBm}")
print(f"V TRP (dBm): {v_TRP_dBm}")
'''
return (data_points, theta_angles_rad, phi_angles_rad, total_power_dBm_2d,

return (data_points, theta_angles_deg, phi_angles_deg, theta_angles_rad, phi_angles_rad, total_power_dBm_2d,
total_power_dBm_min, total_power_dBm_nom, h_power_dBm_2d, h_power_dBm_min, v_power_dBm_2d,
v_power_dBm_min, h_power_dBm_nom, v_power_dBm_nom, TRP_dBm, h_TRP_dBm, v_TRP_dBm)

# _____________Passive Calculation Functions___________
#Auto Determine Polarization for HPOL & VPOL Files
# Auto Determine Polarization for HPOL & VPOL Files
def determine_polarization(file_path):
with open(file_path, 'r') as f:
content = f.read()
if "Horizontal Polarization" in content:
return "HPol"
else:
return "VPol"
#Verify angle data and frequencies are not mismatched

# Verify angle data and frequencies are not mismatched
def angles_match(start_phi_h, stop_phi_h, inc_phi_h, start_theta_h, stop_theta_h, inc_theta_h,
start_phi_v, stop_phi_v, inc_phi_v, start_theta_v, stop_theta_v, inc_theta_v):

return (start_phi_h == start_phi_v and stop_phi_h == stop_phi_v and inc_phi_h == inc_phi_v and
start_theta_h == start_theta_v and stop_theta_h == stop_theta_v and inc_theta_h == inc_theta_v)

#Extract Frequency points for selection in the drop-down menu
# Extract Frequency points for selection in the drop-down menu
def extract_passive_frequencies(file_path):
with open(file_path, 'r') as file:
content = file.readlines()
Expand All @@ -99,18 +91,18 @@ def extract_passive_frequencies(file_path):

return frequencies

#Calculate Total Gain Vector and add cable loss etc - Use Phase for future implementation?
# Calculate Total Gain Vector and add cable loss etc - Use Phase for future implementation?
def calculate_passive_variables(hpol_data, vpol_data, cable_loss, start_phi, stop_phi, inc_phi, start_theta, stop_theta, inc_theta, freq_list, selected_frequency):
theta_points = int((stop_theta - start_theta) / inc_theta + 1)
phi_points = int((stop_phi - start_phi) / inc_phi + 1)
data_points = theta_points * phi_points

theta_angles_deg = np.zeros((data_points, len(freq_list)))
phi_angles_deg = np.zeros((data_points, len(freq_list)))
v_gain_dB = np.zeros((data_points, len(freq_list)))
h_gain_dB = np.zeros((data_points, len(freq_list)))
v_phase = np.zeros((data_points, len(freq_list)))
h_phase = np.zeros((data_points, len(freq_list)))
theta_angles_deg = np.zeros((phi_points * theta_points, len(freq_list)))
phi_angles_deg = np.zeros((phi_points * theta_points, len(freq_list)))
v_gain_dB = np.zeros((phi_points * theta_points, len(freq_list)))
h_gain_dB = np.zeros((phi_points * theta_points, len(freq_list)))
v_phase = np.zeros((phi_points * theta_points, len(freq_list)))
h_phase = np.zeros((phi_points * theta_points, len(freq_list)))

for m, (hpol_entry, vpol_entry) in enumerate(zip(hpol_data, vpol_data)):
for n, (theta_h, phi_h, mag_h, phase_h, theta_v, phi_v, mag_v, phase_v) in enumerate(zip(hpol_entry['theta'], hpol_entry['phi'], hpol_entry['mag'], hpol_entry['phase'], vpol_entry['theta'], vpol_entry['phi'], vpol_entry['mag'], vpol_entry['phase'])):
Expand All @@ -134,3 +126,144 @@ def calculate_passive_variables(hpol_data, vpol_data, cable_loss, start_phi, sto


return theta_angles_deg, phi_angles_deg, v_gain_dB, h_gain_dB, Total_Gain_dB

# Enhanced NF to FF Transformation
def apply_nf2ff_transformation(hpol_data, vpol_data, frequency,
start_phi, stop_phi, inc_phi,
start_theta, stop_theta, inc_theta,
measurement_distance, window_function='none'):
"""
Applies Near-Field to Far-Field transformation using Plane Wave Decomposition.
Parameters:
hpol_data (list): List of dictionaries with 'mag' and 'phase' for horizontal polarization.
vpol_data (list): List of dictionaries with 'mag' and 'phase' for vertical polarization.
frequency (float): Frequency in MHz.
start_phi, stop_phi, inc_phi (float): Phi angle range and increment in degrees.
start_theta, stop_theta, inc_theta (float): Theta angle range and increment in degrees.
measurement_distance (float): Distance from antenna to probe in meters.
window_function (str): Type of window to apply ('none', 'hanning', 'hamming', etc.).
Returns:
hpol_far_field (list): Far-field data for horizontal polarization.
vpol_far_field (list): Far-field data for vertical polarization.
"""
# Calculate wavelength
wavelength = c / (frequency * 1e6) # Convert MHz to Hz

# Prepare theta and phi grids in radians
theta = np.deg2rad(np.arange(start_theta, stop_theta + inc_theta, inc_theta))
phi = np.deg2rad(np.arange(start_phi, stop_phi + inc_phi, inc_phi))
theta_grid, phi_grid = np.meshgrid(theta, phi, indexing='ij')

# Calculate Plane Wave Decomposition coefficients
k = 2 * np.pi / wavelength # Wave number

# Initialize far-field lists
hpol_far_field = []
vpol_far_field = []

# Select window function
window = get_window(window_function, theta_grid.shape)

for hpol_entry, vpol_entry in zip(hpol_data, vpol_data):
# Convert magnitude and phase to complex near-field data
h_near_field = hpol_entry['mag'] * np.exp(1j * np.deg2rad(hpol_entry['phase']))
v_near_field = vpol_entry['mag'] * np.exp(1j * np.deg2rad(vpol_entry['phase']))

# Apply windowing if selected
if window is not None:
h_near_field *= window
v_near_field *= window

# Apply Plane Wave Decomposition
h_ff = plane_wave_decomposition(h_near_field, theta_grid, phi_grid, k, measurement_distance)
v_ff = plane_wave_decomposition(v_near_field, theta_grid, phi_grid, k, measurement_distance)

# Scale far-field based on wavelength and distance
scaling_factor = wavelength / (4 * np.pi * measurement_distance)
h_ff *= scaling_factor
v_ff *= scaling_factor

# Convert to magnitude and phase
h_far_field_mag = np.abs(h_ff)
h_far_field_phase = np.angle(h_ff, deg=True)

v_far_field_mag = np.abs(v_ff)
v_far_field_phase = np.angle(v_ff, deg=True)

# Append to far-field lists
hpol_far_field.append({'mag': h_far_field_mag, 'phase': h_far_field_phase})
vpol_far_field.append({'mag': v_far_field_mag, 'phase': v_far_field_phase})

return hpol_far_field, vpol_far_field

def plane_wave_decomposition(near_field, theta, phi, k, distance):
"""
Performs Plane Wave Decomposition on near-field data to obtain far-field.
Parameters:
near_field (2D np.array): Complex near-field data.
theta (2D np.array): Theta angles in radians.
phi (2D np.array): Phi angles in radians.
k (float): Wave number.
distance (float): Measurement distance in meters.
Returns:
far_field (2D np.array): Complex far-field data.
"""
# Calculate the phase shift based on the distance and angle
# Assuming spherical measurement, phase shift is -j * k * r * cos(theta)
exponent = -1j * k * distance * np.cos(theta)

# Apply the phase shift to decompose into far-field
far_field = near_field * np.exp(exponent)

return far_field

def get_window(window_type, shape):
"""
Generates a windowing function based on the specified type and shape.
Parameters:
window_type (str): Type of window ('none', 'hanning', 'hamming', etc.).
shape (tuple): Shape of the window (rows, cols).
Returns:
window (2D np.array or None): Windowing matrix or None if 'none'.
"""
if window_type.lower() == 'none':
return None
elif window_type.lower() == 'hanning':
window_1d_theta = windows.hann(shape[0])
window_1d_phi = windows.hann(shape[1])
elif window_type.lower() == 'hamming':
window_1d_theta = windows.hamming(shape[0])
window_1d_phi = windows.hamming(shape[1])
else:
raise ValueError(f"Unsupported window type: {window_type}")

# Create 2D window by outer product
window = np.outer(window_1d_theta, window_1d_phi)
return window

# Helper function to process gain data for plotting.
def process_data(selected_data, selected_phi_angles_deg, selected_theta_angles_deg):
"""
Helper function to process data (gain or power) for plotting using interp2d.
"""
# Convert angles to radians
theta = np.deg2rad(selected_theta_angles_deg)
phi = np.deg2rad(selected_phi_angles_deg)

# Create a 2D grid of theta and phi values
theta_grid, phi_grid = np.meshgrid(theta, phi, indexing='ij')

# Perform Plane Wave Decomposition
far_field_complex = np.fft.fftshift(np.fft.fft2(np.fft.ifftshift(selected_data)))

# Normalize the far-field pattern
far_field_mag = np.abs(far_field_complex)
far_field_phase = np.angle(far_field_complex, deg=True)

return far_field_mag, far_field_phase
3 changes: 2 additions & 1 deletion plot_antenna/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
This module contains constants and configuration values used throughout the application.
"""

interpolate_3d_plots = True # Default value, can be set to False to disable interpolation
# 3D Plotting Interpolation Resolution for viewing plots (lower for better performance)
PHI_RESOLUTION = 120 #default 360 = 1deg spacing
THETA_RESOLUTION = 60 #default 180 - 1deg spacing
Expand All @@ -28,3 +28,4 @@
# Fonts
HEADER_FONT = ("Arial", 14, "bold")
LABEL_FONT = ("Arial", 12)

Loading

0 comments on commit ce218c2

Please sign in to comment.