Last active
November 17, 2025 01:47
-
-
Save Onefabis/0dc9a8f962703e269d1cec773b119a4a to your computer and use it in GitHub Desktop.
Antenna plot generator
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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() |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Вы можете сгенерировать .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 самостоятельно, вы можете скачать его здесь.