Skip to content

Instantly share code, notes, and snippets.

@Onefabis
Last active November 17, 2025 01:47
Show Gist options
  • Select an option

  • Save Onefabis/0dc9a8f962703e269d1cec773b119a4a to your computer and use it in GitHub Desktop.

Select an option

Save Onefabis/0dc9a8f962703e269d1cec773b119a4a to your computer and use it in GitHub Desktop.
Antenna plot generator
import sys
import os
from pathlib import Path
from typing import Optional, List
import numpy as np
from scipy.interpolate import PchipInterpolator
import matplotlib
matplotlib.use('QtAgg')
from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure
import matplotlib.pyplot as plt
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import (
QApplication, QWidget, QVBoxLayout, QLabel,
QPushButton, QFileDialog, QHBoxLayout
)
class PlotConfig:
"""Configuration constants for the polar plot."""
R_CENTER = -40
R_OUTER = 0
RADIAL_TICKS = [0, 5, 10, 20, 30, 40]
SMOOTH_POINTS = 360
FIGURE_SIZE = (6, 6)
THETA_OFFSET = np.pi / 2
CURVE_LINEWIDTH = 2
POINT_COLOR = 'red'
class DataLoader:
"""Handles loading and validating antenna data."""
def __init__(self, filename: str = "antenna.txt"):
self.filename = filename
def get_data_path(self) -> Path:
"""Get the path to the data file relative to the executable/script."""
if getattr(sys, 'frozen', False):
base_dir = Path(sys.executable).parent
else:
base_dir = Path(__file__).parent
return base_dir / self.filename
def load(self) -> tuple[Optional[List[float]], Optional[str]]:
"""
Load and validate data from file.
Returns:
tuple: (data list, error message) where error is None if successful
"""
file_path = self.get_data_path()
if not file_path.exists():
return None, f"Error: '{self.filename}' not found next to executable."
try:
with open(file_path, "r") as f:
line = f.readline().strip()
if not line:
return None, "Error: File is empty."
data = [float(x) for x in line.split(",")]
if len(data) < 2:
return None, "Error: Not enough data points to plot."
return data, None
except ValueError:
return None, "Error: File contains invalid data. Must be comma-separated numbers."
except Exception as e:
return None, f"Error reading file: {e}"
class PolarPlotter:
"""Handles the creation and configuration of polar plots."""
def __init__(self, config: PlotConfig = PlotConfig()):
self.config = config
def prepare_data(self, data: List[float]) -> tuple:
"""
Prepare data for plotting with interpolation.
Returns:
tuple: (theta, theta_smooth, r_plot, original_r)
"""
n_points = len(data)
theta = np.linspace(0, 2 * np.pi, n_points, endpoint=True)
theta_smooth = np.linspace(0, 2 * np.pi, self.config.SMOOTH_POINTS)
# Interpolate for smooth curve
pchip = PchipInterpolator(theta, data, extrapolate=True)
data_smooth = pchip(theta_smooth)
# Transform to correct radial scale (0 dB at perimeter, -40 dB at center)
r_plot = data_smooth - self.config.R_CENTER
original_r = np.array(data) - self.config.R_CENTER
return theta, theta_smooth, r_plot, original_r
def configure_axis(self, ax):
"""Configure polar axis with proper labels and limits."""
ax.set_ylim(0, self.config.R_OUTER - self.config.R_CENTER)
ax.set_yticks(self.config.RADIAL_TICKS)
ax.set_yticklabels([
str(int(self.config.R_CENTER + t))
for t in self.config.RADIAL_TICKS
])
def plot_on_axis(self, ax, data: List[float]):
"""Plot data on the given axis."""
theta, theta_smooth, r_plot, original_r = self.prepare_data(data)
# Plot smooth curve and original points
ax.plot(theta_smooth, r_plot, linewidth=self.config.CURVE_LINEWIDTH)
ax.plot(theta, original_r, 'o', color=self.config.POINT_COLOR)
self.configure_axis(ax)
def create_figure(self, data: List[float]) -> Figure:
"""Create a standalone figure with the plot."""
fig = plt.figure(figsize=self.config.FIGURE_SIZE)
ax = fig.add_subplot(
1, 1, 1,
projection='polar',
theta_offset=self.config.THETA_OFFSET
)
self.plot_on_axis(ax, data)
return fig
class AntennaPlot(QWidget):
"""Main application window for antenna polar plots."""
def __init__(self):
super().__init__()
self.data: Optional[List[float]] = None
self.loader = DataLoader()
self.plotter = PolarPlotter()
self._setup_ui()
self.plot_data()
def _setup_ui(self):
"""Initialize the user interface."""
self.setWindowTitle("Antenna Polar Plot")
self.resize(700, 580)
# Main layout
layout = QVBoxLayout()
layout.setAlignment(Qt.AlignmentFlag.AlignTop)
self.setLayout(layout)
# Error label
self.error_label = QLabel()
layout.addWidget(self.error_label)
# Matplotlib canvas
self.canvas = FigureCanvas(Figure(figsize=(5, 5)))
layout.addWidget(self.canvas)
self.ax = self.canvas.figure.add_subplot(
1, 1, 1,
projection='polar',
theta_offset=PlotConfig.THETA_OFFSET
)
# Buttons
btn_layout = QHBoxLayout()
layout.addLayout(btn_layout)
self.reload_btn = QPushButton("Reload")
self.save_btn = QPushButton("Save")
btn_layout.addWidget(self.reload_btn)
btn_layout.addWidget(self.save_btn)
# Connect signals
self.reload_btn.clicked.connect(self.plot_data)
self.save_btn.clicked.connect(self.save_plot)
def plot_data(self):
"""Load and plot antenna data."""
self.ax.clear()
self.error_label.clear()
# Load data
self.data, error = self.loader.load()
if error:
self.error_label.setText(error)
self.canvas.draw()
return
# Plot
try:
self.plotter.plot_on_axis(self.ax, self.data)
self.canvas.draw()
except Exception as e:
self.error_label.setText(f"Error plotting data: {e}")
def save_plot(self):
"""Save the current plot to an image file."""
if self.data is None:
self.error_label.setText("No data to save.")
return
# Get save filename
filename, _ = QFileDialog.getSaveFileName(
self,
"Save Plot As Image",
"",
"PNG Files (*.png);;JPEG Files (*.jpg);;All Files (*)",
options=QFileDialog.Option.DontUseNativeDialog
)
if not filename:
return
# Create and save figure
try:
matplotlib.use('Agg') # Non-GUI backend for saving
fig = self.plotter.create_figure(self.data)
fig.savefig(filename, dpi=150, bbox_inches='tight')
plt.close(fig)
matplotlib.use('QtAgg') # Restore GUI backend
self.error_label.setText(f"Saved successfully to {Path(filename).name}")
except Exception as e:
self.error_label.setText(f"Error saving image: {e}")
def main():
"""Application entry point."""
app = QApplication(sys.argv)
win = AntennaPlot()
win.show()
sys.exit(app.exec())
if __name__ == "__main__":
main()
@Onefabis
Copy link
Author

Onefabis commented Nov 17, 2025

Вы можете сгенерировать .exe-файл. Просто установите пакеты командой pip -install numpy scipy matplotlib PyQt6. Затем установите pyinstaller командой pip -install pyinstaller и выполните: pyinstaller --onefile --windowed antenna_plot.py

Не забудьте создать файл antenna.txt и поместить туда набор чисел в диапазоне от –40 до 0 дБ, основанных на ваших измерениях антенны, например: 0, -5, -10, -20, -10, -5, 0.
Это означает:
0 дБ при 0°,
–5 дБ при 72°,
–10 дБ при 144°,
–20 дБ при 216°,
–10 дБ при 288°,
0 дБ при 360°.

То есть вам нужно ввести данные для углов 0–360°. Но шаг может быть любым, не обязательно 72°. Основное правило: количество измерений должно быть нечётным, причём первое значение соответствует 0° на графике, а последнее — 360°.

Если вы не хотите собирать .exe самостоятельно, вы можете скачать его здесь.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment