-
-
Save Onefabis/0dc9a8f962703e269d1cec773b119a4a to your computer and use it in GitHub Desktop.
| 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() |
Вы можете сгенерировать .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 самостоятельно, вы можете скачать его здесь.
You can generate .exe file. Just pip -install numpy, scipy, matplotlib and PyQt6. Then install with pip -install pyinstaller and run the command:
pyinstaller --onefile --windowed antenna_plot.pyDon't forget to create antenna.txt file and put some numbers from in range -40 to 0 dB, based on your antenna measurements: 0,-5,-10,-20,-10,-5, 0. Which is mean that:
0 dB at 0°,
–5 dB at 72°,
–10 dB at 144°,
–20 dB at 216°,
–10 dB at 288°,
0 dB at 360°.
So you need to enter 0-360 degree data. But that step should be any, not only 72 degree. The main rule is that measurements should be odd and the first one will be at 0 degree on the plot and the last one - at 360 degree.
You can download .exe file here if you don't want to build it by yourself.