Source code for phased_array.export

"""
Export functions for phased array antenna data.

Supports CSV, JSON, NumPy, and MATLAB formats.
"""

import io
import json
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, Optional, Tuple, Union

import numpy as np

from .geometry import ArrayGeometry


[docs] def export_pattern_csv( angles: np.ndarray, pattern_dB: np.ndarray, filename: Optional[str] = None, angle_label: str = "angle_deg", pattern_label: str = "pattern_dB", include_header: bool = True, metadata: Optional[Dict[str, Any]] = None ) -> str: """ Export 1D pattern data to CSV format. Parameters ---------- angles : ndarray Angle values (typically in degrees) pattern_dB : ndarray Pattern values in dB filename : str, optional If provided, write to file. Otherwise return string. angle_label : str Column label for angles pattern_label : str Column label for pattern include_header : bool Whether to include column headers metadata : dict, optional Additional metadata to include as comments Returns ------- csv_string : str CSV formatted string (also written to file if filename provided) Examples -------- Export a pattern cut to CSV: >>> import numpy as np >>> import phased_array as pa >>> geom = pa.create_rectangular_array(8, 8, dx=0.5, dy=0.5) >>> k = pa.wavelength_to_k(1.0) >>> weights = pa.steering_vector(k, geom.x, geom.y, theta0_deg=0, phi0_deg=0) >>> theta_deg, E_plane, H_plane = pa.compute_pattern_cuts( ... geom.x, geom.y, weights, k ... ) >>> csv_str = pa.export_pattern_csv( ... theta_deg, E_plane, ... angle_label='theta_deg', ... pattern_label='E_plane_dB', ... metadata={'array_size': '8x8', 'scan_angle': '0 deg'} ... ) Save to file: >>> # pa.export_pattern_csv(theta_deg, E_plane, filename='pattern.csv') """ buffer = io.StringIO() # Write metadata as comments if metadata: buffer.write(f"# Phased Array Pattern Export\n") buffer.write(f"# Generated: {datetime.now().isoformat()}\n") for key, value in metadata.items(): buffer.write(f"# {key}: {value}\n") buffer.write("#\n") # Write header if include_header: buffer.write(f"{angle_label},{pattern_label}\n") # Write data for angle, pattern in zip(angles, pattern_dB): buffer.write(f"{angle:.6f},{pattern:.4f}\n") csv_string = buffer.getvalue() if filename: with open(filename, 'w') as f: f.write(csv_string) return csv_string
[docs] def export_pattern_2d_csv( theta: np.ndarray, phi: np.ndarray, pattern_dB: np.ndarray, filename: Optional[str] = None, include_header: bool = True, metadata: Optional[Dict[str, Any]] = None ) -> str: """ Export 2D pattern data to CSV format. Parameters ---------- theta : ndarray Theta values (1D or meshgrid) phi : ndarray Phi values (1D or meshgrid) pattern_dB : ndarray 2D pattern in dB filename : str, optional If provided, write to file include_header : bool Whether to include column headers metadata : dict, optional Additional metadata as comments Returns ------- csv_string : str CSV formatted string """ buffer = io.StringIO() # Write metadata if metadata: buffer.write(f"# Phased Array 2D Pattern Export\n") buffer.write(f"# Generated: {datetime.now().isoformat()}\n") for key, value in metadata.items(): buffer.write(f"# {key}: {value}\n") buffer.write("#\n") # Write header if include_header: buffer.write("theta_deg,phi_deg,pattern_dB\n") # Handle meshgrid or 1D arrays if theta.ndim == 1 and phi.ndim == 1: theta_grid, phi_grid = np.meshgrid(theta, phi, indexing='ij') else: theta_grid, phi_grid = theta, phi # Flatten and write for i in range(theta_grid.shape[0]): for j in range(theta_grid.shape[1]): buffer.write(f"{theta_grid[i,j]:.4f},{phi_grid[i,j]:.4f},{pattern_dB[i,j]:.4f}\n") csv_string = buffer.getvalue() if filename: with open(filename, 'w') as f: f.write(csv_string) return csv_string
[docs] def export_uv_pattern_csv( u: np.ndarray, v: np.ndarray, pattern_dB: np.ndarray, filename: Optional[str] = None, include_header: bool = True, metadata: Optional[Dict[str, Any]] = None ) -> str: """ Export UV-space pattern data to CSV format. Parameters ---------- u : ndarray U direction cosine values v : ndarray V direction cosine values pattern_dB : ndarray 2D pattern in dB filename : str, optional If provided, write to file include_header : bool Whether to include column headers metadata : dict, optional Additional metadata Returns ------- csv_string : str CSV formatted string """ buffer = io.StringIO() if metadata: buffer.write(f"# Phased Array UV-Space Pattern Export\n") buffer.write(f"# Generated: {datetime.now().isoformat()}\n") for key, value in metadata.items(): buffer.write(f"# {key}: {value}\n") buffer.write("#\n") if include_header: buffer.write("u,v,pattern_dB\n") # Handle meshgrid or 1D arrays if u.ndim == 1 and v.ndim == 1: for i in range(len(u)): for j in range(len(v)): buffer.write(f"{u[i]:.6f},{v[j]:.6f},{pattern_dB[i,j]:.4f}\n") else: for i in range(u.shape[0]): for j in range(u.shape[1]): buffer.write(f"{u[i,j]:.6f},{v[i,j]:.6f},{pattern_dB[i,j]:.4f}\n") csv_string = buffer.getvalue() if filename: with open(filename, 'w') as f: f.write(csv_string) return csv_string
[docs] def export_weights_csv( geometry: ArrayGeometry, weights: np.ndarray, filename: Optional[str] = None, include_header: bool = True, metadata: Optional[Dict[str, Any]] = None ) -> str: """ Export element weights to CSV format. Parameters ---------- geometry : ArrayGeometry Array geometry with element positions weights : ndarray Complex element weights filename : str, optional If provided, write to file include_header : bool Whether to include column headers metadata : dict, optional Additional metadata Returns ------- csv_string : str CSV formatted string with columns: element, x, y, weight_real, weight_imag, weight_mag, weight_phase_deg """ buffer = io.StringIO() if metadata: buffer.write(f"# Phased Array Weights Export\n") buffer.write(f"# Generated: {datetime.now().isoformat()}\n") buffer.write(f"# N_elements: {geometry.n_elements}\n") for key, value in metadata.items(): buffer.write(f"# {key}: {value}\n") buffer.write("#\n") if include_header: buffer.write("element,x,y,weight_real,weight_imag,weight_mag,weight_phase_deg\n") for i in range(geometry.n_elements): w = weights[i] buffer.write(f"{i},{geometry.x[i]:.6f},{geometry.y[i]:.6f}," f"{w.real:.8f},{w.imag:.8f}," f"{np.abs(w):.8f},{np.rad2deg(np.angle(w)):.4f}\n") csv_string = buffer.getvalue() if filename: with open(filename, 'w') as f: f.write(csv_string) return csv_string
[docs] def export_geometry_csv( geometry: ArrayGeometry, filename: Optional[str] = None, include_header: bool = True, metadata: Optional[Dict[str, Any]] = None ) -> str: """ Export array geometry to CSV format. Parameters ---------- geometry : ArrayGeometry Array geometry with element positions filename : str, optional If provided, write to file include_header : bool Whether to include column headers metadata : dict, optional Additional metadata Returns ------- csv_string : str CSV formatted string """ buffer = io.StringIO() if metadata: buffer.write(f"# Phased Array Geometry Export\n") buffer.write(f"# Generated: {datetime.now().isoformat()}\n") buffer.write(f"# N_elements: {geometry.n_elements}\n") for key, value in metadata.items(): buffer.write(f"# {key}: {value}\n") buffer.write("#\n") # Check if we have 3D geometry and normal vectors has_z = geometry.z is not None has_normals = hasattr(geometry, 'nx') and geometry.nx is not None if include_header: header_parts = ["element", "x", "y"] if has_z: header_parts.append("z") if has_normals: header_parts.extend(["nx", "ny", "nz"]) buffer.write(",".join(header_parts) + "\n") for i in range(geometry.n_elements): row = [str(i), f"{geometry.x[i]:.6f}", f"{geometry.y[i]:.6f}"] if has_z: row.append(f"{geometry.z[i]:.6f}") if has_normals: row.extend([f"{geometry.nx[i]:.6f}", f"{geometry.ny[i]:.6f}", f"{geometry.nz[i]:.6f}"]) buffer.write(",".join(row) + "\n") csv_string = buffer.getvalue() if filename: with open(filename, 'w') as f: f.write(csv_string) return csv_string
[docs] def export_array_config_json( geometry: ArrayGeometry, weights: Optional[np.ndarray] = None, array_params: Optional[Dict[str, Any]] = None, steering: Optional[Dict[str, float]] = None, taper_info: Optional[Dict[str, Any]] = None, filename: Optional[str] = None ) -> str: """ Export full array configuration to JSON format. Parameters ---------- geometry : ArrayGeometry Array geometry weights : ndarray, optional Complex weights (exported as mag/phase) array_params : dict, optional Array parameters (type, spacing, etc.) steering : dict, optional Steering direction (theta, phi) taper_info : dict, optional Taper information filename : str, optional If provided, write to file Returns ------- json_string : str JSON formatted string """ config = { "metadata": { "export_time": datetime.now().isoformat(), "format_version": "1.0", "package": "phased-array-modeling" }, "geometry": { "n_elements": geometry.n_elements, "x": geometry.x.tolist(), "y": geometry.y.tolist(), } } if geometry.z is not None: config["geometry"]["z"] = geometry.z.tolist() if hasattr(geometry, 'nx') and geometry.nx is not None: config["geometry"]["nx"] = geometry.nx.tolist() config["geometry"]["ny"] = geometry.ny.tolist() config["geometry"]["nz"] = geometry.nz.tolist() if weights is not None: config["weights"] = { "magnitude": np.abs(weights).tolist(), "phase_deg": np.rad2deg(np.angle(weights)).tolist() } if array_params: config["array_params"] = array_params if steering: config["steering"] = steering if taper_info: config["taper"] = taper_info json_string = json.dumps(config, indent=2) if filename: with open(filename, 'w') as f: f.write(json_string) return json_string
[docs] def export_pattern_npz( filename: str, angles: Optional[np.ndarray] = None, e_plane: Optional[np.ndarray] = None, h_plane: Optional[np.ndarray] = None, theta: Optional[np.ndarray] = None, phi: Optional[np.ndarray] = None, pattern_2d: Optional[np.ndarray] = None, u: Optional[np.ndarray] = None, v: Optional[np.ndarray] = None, pattern_uv: Optional[np.ndarray] = None, weights: Optional[np.ndarray] = None, geometry_x: Optional[np.ndarray] = None, geometry_y: Optional[np.ndarray] = None, metadata: Optional[Dict[str, Any]] = None ) -> None: """ Export pattern data to compressed NumPy format. Parameters ---------- filename : str Output filename (should end in .npz) angles : ndarray, optional 1D cut angles e_plane : ndarray, optional E-plane pattern h_plane : ndarray, optional H-plane pattern theta, phi : ndarray, optional 2D grid coordinates pattern_2d : ndarray, optional Full 2D pattern u, v : ndarray, optional UV-space coordinates pattern_uv : ndarray, optional UV-space pattern weights : ndarray, optional Complex element weights geometry_x, geometry_y : ndarray, optional Element positions metadata : dict, optional Additional metadata (converted to string for storage) """ data = {} if angles is not None: data['angles'] = angles if e_plane is not None: data['e_plane'] = e_plane if h_plane is not None: data['h_plane'] = h_plane if theta is not None: data['theta'] = theta if phi is not None: data['phi'] = phi if pattern_2d is not None: data['pattern_2d'] = pattern_2d if u is not None: data['u'] = u if v is not None: data['v'] = v if pattern_uv is not None: data['pattern_uv'] = pattern_uv if weights is not None: data['weights'] = weights if geometry_x is not None: data['geometry_x'] = geometry_x if geometry_y is not None: data['geometry_y'] = geometry_y if metadata is not None: data['metadata'] = np.array([json.dumps(metadata)]) np.savez_compressed(filename, **data)
[docs] def load_pattern_npz(filename: str) -> Dict[str, Any]: """ Load pattern data from NumPy format. Parameters ---------- filename : str Input filename Returns ------- data : dict Dictionary with loaded arrays and metadata """ loaded = np.load(filename, allow_pickle=True) data = {key: loaded[key] for key in loaded.files} # Parse metadata if present if 'metadata' in data: try: data['metadata'] = json.loads(str(data['metadata'][0])) except: pass return data
[docs] def export_coupling_matrix_csv( coupling_matrix: np.ndarray, filename: Optional[str] = None, format: str = 'magnitude_phase', metadata: Optional[Dict[str, Any]] = None ) -> str: """ Export mutual coupling matrix to CSV format. Parameters ---------- coupling_matrix : ndarray Complex N x N coupling matrix filename : str, optional If provided, write to file format : str 'magnitude_phase' - two columns per element (mag, phase_deg) 'real_imag' - two columns per element (real, imag) 'magnitude' - magnitude only 'dB' - magnitude in dB only metadata : dict, optional Additional metadata Returns ------- csv_string : str CSV formatted string """ buffer = io.StringIO() n = coupling_matrix.shape[0] if metadata: buffer.write(f"# Mutual Coupling Matrix Export\n") buffer.write(f"# Generated: {datetime.now().isoformat()}\n") buffer.write(f"# Format: {format}\n") buffer.write(f"# Size: {n} x {n}\n") for key, value in metadata.items(): buffer.write(f"# {key}: {value}\n") buffer.write("#\n") if format == 'magnitude': buffer.write(",".join([f"col_{j}" for j in range(n)]) + "\n") for i in range(n): row = [f"{np.abs(coupling_matrix[i,j]):.8f}" for j in range(n)] buffer.write(",".join(row) + "\n") elif format == 'dB': buffer.write(",".join([f"col_{j}" for j in range(n)]) + "\n") for i in range(n): row = [f"{20*np.log10(max(np.abs(coupling_matrix[i,j]), 1e-10)):.4f}" for j in range(n)] buffer.write(",".join(row) + "\n") elif format == 'magnitude_phase': buffer.write("row,col,magnitude,phase_deg\n") for i in range(n): for j in range(n): c = coupling_matrix[i, j] buffer.write(f"{i},{j},{np.abs(c):.8f},{np.rad2deg(np.angle(c)):.4f}\n") elif format == 'real_imag': buffer.write("row,col,real,imag\n") for i in range(n): for j in range(n): c = coupling_matrix[i, j] buffer.write(f"{i},{j},{c.real:.8f},{c.imag:.8f}\n") csv_string = buffer.getvalue() if filename: with open(filename, 'w') as f: f.write(csv_string) return csv_string
[docs] def export_summary_report( geometry: ArrayGeometry, weights: np.ndarray, pattern_metrics: Dict[str, float], array_params: Optional[Dict[str, Any]] = None, steering: Optional[Dict[str, float]] = None, filename: Optional[str] = None ) -> str: """ Generate a human-readable summary report. Parameters ---------- geometry : ArrayGeometry Array geometry weights : ndarray Complex weights pattern_metrics : dict Dictionary with metrics like HPBW, SLL, directivity, etc. array_params : dict, optional Array configuration parameters steering : dict, optional Steering direction filename : str, optional If provided, write to file Returns ------- report : str Formatted text report """ lines = [] lines.append("=" * 60) lines.append("PHASED ARRAY ANTENNA - SUMMARY REPORT") lines.append("=" * 60) lines.append(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") lines.append("") # Array Configuration lines.append("-" * 40) lines.append("ARRAY CONFIGURATION") lines.append("-" * 40) lines.append(f"Number of Elements: {geometry.n_elements}") aperture_x = geometry.x.max() - geometry.x.min() aperture_y = geometry.y.max() - geometry.y.min() lines.append(f"Aperture Size: {aperture_x:.3f} x {aperture_y:.3f} m") if array_params: for key, value in array_params.items(): if key != 'wavelength': lines.append(f"{key}: {value}") if 'wavelength' in array_params: lines.append(f"Wavelength: {array_params['wavelength']} m") lines.append("") # Steering if steering: lines.append("-" * 40) lines.append("BEAM STEERING") lines.append("-" * 40) lines.append(f"Theta (elevation): {steering.get('theta', 0)}°") lines.append(f"Phi (azimuth): {steering.get('phi', 0)}°") lines.append("") # Pattern Metrics lines.append("-" * 40) lines.append("PATTERN METRICS") lines.append("-" * 40) for key, value in pattern_metrics.items(): if isinstance(value, float): lines.append(f"{key}: {value:.2f}") else: lines.append(f"{key}: {value}") lines.append("") # Weight Statistics lines.append("-" * 40) lines.append("WEIGHT STATISTICS") lines.append("-" * 40) mag = np.abs(weights) phase = np.rad2deg(np.angle(weights)) lines.append(f"Magnitude Range: {mag.min():.4f} - {mag.max():.4f}") lines.append(f"Phase Range: {phase.min():.1f}° - {phase.max():.1f}°") lines.append(f"Total Power: {np.sum(mag**2):.4f}") lines.append("") lines.append("=" * 60) lines.append("END OF REPORT") lines.append("=" * 60) report = "\n".join(lines) if filename: with open(filename, 'w') as f: f.write(report) return report