Skip to content

Instantly share code, notes, and snippets.

@GamerKingFaiz
Last active June 17, 2025 00:05
Show Gist options
  • Save GamerKingFaiz/6e7042517a39e012bcdc316975026947 to your computer and use it in GitHub Desktop.
Save GamerKingFaiz/6e7042517a39e012bcdc316975026947 to your computer and use it in GitHub Desktop.
Visualize UniFi AP Antenna Radiation in 3D

Intro

I tried to find if anyone else had tried to do this and found this forum post, which seemed to indicate that you could view the visualization of the antenna file (.ant) that Ubiquiti publishes using an old program called Octave, but I wasn't able to get that running on neither Windows nor Mac.

That post did give a hint that the ant files are using Radio Mobile's V3 format, though it was an assumption by the original poster.
Important notes to pull from the V3 format docs:

The antenna file formatted for V3 has 720 lines.

The first 360 lines describe the horizontal plane of the radiation pattern. Starting at the front beam from 0 to 359 degrees. The horizontal plane of the pattern is level.

The second 360 lines describe the vertical plane of the radiation pattern.

The description of the vertical plane starts at +90 degrees where 0 degrees is level. See the blue arrow in the diagram below. The vertical angle starts at +90 degrees on line 361. The order is +90, 0, -90,-180, +180,+89.

Not being super familiar with the subject matter, I turned to ChatGPT and after lots of trial and error I think I was able to get it to spit out a Python script that seems to generate what look like accurate 3D visuals. For example, in the image of the U7 In-Wall, there's a big dead zone right behind the AP, which can be corroborated with what the UI Design Tool shows.

Call to Action

If you're familiar with any of these subjects (Python visualization, cartesian math, antenna file parsing, etc), please chime in with comments/suggestions!

Script Instructions

You need to change the name of the .ant file in the main function (and add a file path if yours isn't in the same directory as the script).

Install plotly and numpy with pip and then run the script:

pip install plotly numpy
python antenna_visualizer.py
import numpy as np
import plotly.graph_objs as go
import os
def read_ant_file(filename):
with open(filename, 'r', encoding='utf-16') as f:
lines = f.readlines()
# Convert to float and strip BOM if present
gains = [float(line.strip().replace('\ufeff', '')) for line in lines if line.strip()]
if len(gains) != 720:
raise ValueError("Expected 720 gain values (360 azimuth + 360 elevation)")
return np.array(gains[:360]), np.array(gains[360:])
def create_gain_grid(dbiAz, dbiEl):
az_deg = np.linspace(0, 360, 361) # phi: azimuth (horizontal)
el_deg = np.linspace(0, 360, 361) # theta: vertical rotation
dbiAz = np.append(dbiAz, dbiAz[0])
dbiEl = np.append(dbiEl, dbiEl[0])
dbiGrid = np.add.outer(dbiEl, dbiAz) # vertical x azimuth
return az_deg, el_deg, dbiGrid
def spherical_to_cartesian(az, el, r):
az_rad = np.radians(az)
el_rad = np.radians(el)
x = r * np.cos(el_rad) * np.cos(az_rad)
y = r * np.cos(el_rad) * np.sin(az_rad)
z = r * np.sin(el_rad)
return x, y, z
def plot_3d_radiation(az_deg, el_deg, dbiGrid, filename):
phi, theta = np.meshgrid(np.radians(az_deg), np.radians(el_deg))
R = dbiGrid - dbiGrid.min() + 1.0 # Normalize gain
X = R * np.sin(theta) * np.cos(phi)
Y = R * np.sin(theta) * np.sin(phi)
Z = R * np.cos(theta)
surface = go.Surface(
x=X, y=Y, z=Z,
surfacecolor=dbiGrid,
colorscale='YlOrRd',
cmin=dbiGrid.min(),
cmax=dbiGrid.max(),
colorbar=dict(title='Gain (dBi)'),
showscale=True,
opacity=0.6,
lighting=dict(ambient=0.6, diffuse=0.5),
)
origin_marker = go.Scatter3d(
x=[0], y=[0], z=[0],
mode='markers+text',
marker=dict(size=5, color='blue', symbol='circle'),
text=['AP'],
textposition='top center',
name='Access Point'
)
layout = go.Layout(
title=f'3D Antenna Radiation Pattern - {filename}',
scene=dict(
xaxis_title='X',
yaxis_title='Y',
zaxis_title='Z',
aspectmode='manual',
aspectratio=dict(x=1.1, y=1.1, z=1.0)
)
)
fig = go.Figure(data=[surface, origin_marker], layout=layout)
fig.show()
def main():
filepath = 'U7-Pro-XG-5GHz.ant'
az_gain, el_gain = read_ant_file(filepath)
az_deg, el_deg, dbiGrid = create_gain_grid(az_gain, el_gain)
base_name = os.path.splitext(os.path.basename(filepath))[0]
plot_3d_radiation(az_deg, el_deg, dbiGrid, base_name)
if __name__ == '__main__':
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment