import numpy as np from typing import Tuple, Optional from .base_array import * def create_ula( elem_num: int, spacing: float, position_errors: Optional[np.ndarray] = None, gain_errors: Optional[np.ndarray] = None, phase_errors: Optional[np.ndarray] = None ) -> Array1D: """ Create a Uniform Linear Array (ULA) along the x-axis with specified element count and spacing. Args: elem_num (int): Number of elements M (must be a positive integer). spacing (float): Distance between adjacent elements (in meters or wavelength units, must be > 0). position_errors (Optional[np.ndarray]): Optional perturbation offsets for each element, (M,), (2, M), or (3, M). gain_errors (Optional[np.ndarray]): Optional gain error factors (amplitude scale), shape (M,). phase_errors (Optional[np.ndarray]): Optional phase errors (in radians), shape (M,). Returns: Array1D: Initialized ULA object with 1D position array. Raises: ValueError: If elem_num is not a positive integer or spacing is not positive. Notes: - Element positions are on the x-axis only. - `positions` is a 1D NumPy array of shape (M,), i.e., scalar x-coordinates. """ if not isinstance(elem_num, int) or elem_num <= 0: raise ValueError(f"elem_num must be a positive integer, got {elem_num}.") if not isinstance(spacing, (float, int)) or spacing <= 0: raise ValueError(f"spacing must be a positive number, got {spacing}.") # Uniform 1D positions: [0, spacing, ..., (M-1)*spacing] indices = np.arange(elem_num) positions = spacing * indices # shape: (M,) return Array1D( positions=positions, type=ArrayType.ULA, spacing=spacing, position_errors=position_errors, gain_errors=gain_errors, phase_errors=phase_errors ) def create_ura( array_size: Tuple[int, int], spacing: float, position_errors: Optional[np.ndarray] = None, gain_errors: Optional[np.ndarray] = None, phase_errors: Optional[np.ndarray] = None ) -> Array2D: """ Create a Uniform Rectangular Array (URA) in the xy-plane. The array consists of elements placed on a 2D rectangular grid, with uniform spacing in both x and y directions. The array lies entirely in the z=0 plane. Args: array_size (Tuple[int, int]): Number of elements along (cols, rows), i.e., (x-axis, y-axis). The total number of elements is M = cols * rows. spacing (float): Distance between adjacent elements (in meters or wavelength units). position_errors (Optional[np.ndarray]): Optional position perturbations of shape (2, M) or (3, M). gain_errors (Optional[np.ndarray]): Optional gain errors of shape (M,). phase_errors (Optional[np.ndarray]): Optional phase errors (in radians), shape (M,). Returns: Array2D: Initialized uniform rectangular array object. Raises: ValueError: If array dimensions are not positive integers or spacing is not positive. Notes: - Output positions are of shape (2, M), each column is [x; y] of an element. - Elements are ordered row-first: for each row, elements increase left to right (x). - The array is centered at the origin only if manually shifted after creation. """ num_cols, num_rows = array_size if not isinstance(num_cols, int) or num_cols <= 0 or not isinstance(num_cols, int) or num_cols <= 0: raise ValueError(f"array_size must be two positive integers, got ({num_cols}, {num_cols}).") if not isinstance(spacing, (float, int)) or spacing <= 0: raise ValueError(f"spacing must be positive, got {spacing}.") total_elements = num_cols * num_rows # Generate 2D grid coordinates in xy-plane x_coords = spacing * np.arange(num_cols) # x: fast-changing axis y_coords = spacing * np.arange(num_rows) # y: slow-changing axis xv, yv = np.meshgrid(x_coords, y_coords) # shape (cols, rows) # Flatten and stack into (2, M) position matrix positions = np.zeros((2, total_elements), dtype=float) positions[0, :] = xv.flatten() # x-axis positions[1, :] = yv.flatten() # y-axis return Array2D( positions=positions, type=ArrayType.URA, spacing=spacing, position_errors=position_errors, gain_errors=gain_errors, phase_errors=phase_errors ) def create_uca( elem_num: int, radius: float, position_errors: Optional[np.ndarray] = None, gain_errors: Optional[np.ndarray] = None, phase_errors: Optional[np.ndarray] = None ) -> Array2D: """ Create a Uniform Circular Array (UCA) centered at the origin in the xy-plane. The array elements are evenly spaced along the circumference of a circle, with the first element at angle 0 (x-axis), and proceeding counterclockwise. Args: elem_num (int): Number of elements M (must be a positive integer). radius (float): Radius of the circular array (in meters or wavelength units, must be > 0). position_errors (Optional[np.ndarray]): Optional position perturbations, shape (2, M) or (3, M). gain_errors (Optional[np.ndarray]): Optional gain error factors (amplitude scale), shape (M,). phase_errors (Optional[np.ndarray]): Optional phase errors (in radians), shape (M,). Returns: Array2D: Initialized array object with circular geometry. Raises: ValueError: - If elem_num is not a positive integer. - If radius is not a positive number. Notes: - The output positions are of shape (2, M), representing [x; y] coordinates. - The nominal element spacing (along the arc) is returned as the 'spacing' field. - The array lies entirely in the xy-plane (z = 0 is implied). """ if not isinstance(elem_num, int) or elem_num <= 0: raise ValueError(f"elem_num ({elem_num}) must be a positive integer.") if not isinstance(radius, (float, int)) or radius <= 0: raise ValueError(f"radius ({radius}) must be a positive number.") # Compute angular positions (uniform spacing) theta = np.linspace(0, 2 * np.pi, elem_num, endpoint=False) # Compute Cartesian coordinates in 2D x = radius * np.cos(theta) y = radius * np.sin(theta) # Store as 2×N position matrix: positions[0, :] = x, positions[1, :] = y positions = np.zeros((2, elem_num), dtype=float) positions[0, :] = x positions[1, :] = y # Compute approximate arc-length spacing between adjacent elements step = 2 * np.pi / elem_num spacing = 2 * radius * np.sin(step / 2) # chord length ≈ arc length for small step return Array2D( positions=positions, type=ArrayType.UCA, spacing=spacing, position_errors=position_errors, gain_errors=gain_errors, phase_errors=phase_errors ) def create_cyl_array_with_uca( elem_per_layer: int, radius: float, num_layers: int, layer_spacing: float, gain_errors: Optional[np.ndarray] = None, phase_errors: Optional[np.ndarray] = None, position_errors: Optional[np.ndarray] = None ) -> Array3D: """ Create a cylindrical array composed of stacked UCAs along the z-axis. Args: elem_per_layer (int): Number of elements per circular UCA layer (must be > 0). radius (float): Radius of each UCA layer (must be > 0). num_layers (int): Number of stacked layers along z-axis (must be > 0). layer_spacing (float): Distance between adjacent layers along z-axis (must be > 0). gain_errors (Optional[np.ndarray]): Optional gain errors, shape (M,). phase_errors (Optional[np.ndarray]): Optional phase errors, shape (M,). position_errors (Optional[np.ndarray]): Optional position errors, shape (3, M). Returns: Array3D: Array object with cylindrical geometry. Raises: ValueError: If any input is invalid (non-positive integer or float). """ if elem_per_layer <= 0 or num_layers <= 0: raise ValueError("elem_per_layer and num_layers must be positive integers.") if radius <= 0: raise ValueError("radius must be a positive number.") if layer_spacing <= 0: raise ValueError("layer_spacing must be a positive number.") total_elements = elem_per_layer * num_layers positions = np.zeros((3, total_elements), dtype=float) theta = np.linspace(0, 2 * np.pi, elem_per_layer, endpoint=False) x = radius * np.cos(theta) y = radius * np.sin(theta) for i in range(num_layers): z = layer_spacing * i idx_start = i * elem_per_layer idx_end = (i + 1) * elem_per_layer positions[0, idx_start:idx_end] = x positions[1, idx_start:idx_end] = y positions[2, idx_start:idx_end] = z return Array3D( positions=positions, type=ArrayType.CYLINDRICAL, spacing=layer_spacing, position_errors=position_errors, gain_errors=gain_errors, phase_errors=phase_errors )