phased-array-modeling 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- phased_array/__init__.py +263 -0
- phased_array/beamforming.py +705 -0
- phased_array/core.py +598 -0
- phased_array/geometry.py +943 -0
- phased_array/impairments.py +719 -0
- phased_array/utils.py +346 -0
- phased_array/visualization.py +824 -0
- phased_array_modeling-1.0.0.dist-info/METADATA +259 -0
- phased_array_modeling-1.0.0.dist-info/RECORD +12 -0
- phased_array_modeling-1.0.0.dist-info/WHEEL +5 -0
- phased_array_modeling-1.0.0.dist-info/licenses/LICENSE +21 -0
- phased_array_modeling-1.0.0.dist-info/top_level.txt +1 -0
phased_array/utils.py
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utility functions for phased array antenna computations.
|
|
3
|
+
|
|
4
|
+
Includes coordinate transforms and helper functions.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
from typing import Tuple, Union
|
|
9
|
+
|
|
10
|
+
ArrayLike = Union[np.ndarray, float]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def deg2rad(degrees: ArrayLike) -> ArrayLike:
|
|
14
|
+
"""Convert degrees to radians."""
|
|
15
|
+
return np.deg2rad(degrees)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def rad2deg(radians: ArrayLike) -> ArrayLike:
|
|
19
|
+
"""Convert radians to degrees."""
|
|
20
|
+
return np.rad2deg(radians)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def azel_to_thetaphi(az: ArrayLike, el: ArrayLike) -> Tuple[ArrayLike, ArrayLike]:
|
|
24
|
+
"""
|
|
25
|
+
Convert azimuth/elevation to theta/phi (spherical coordinates).
|
|
26
|
+
|
|
27
|
+
Parameters
|
|
28
|
+
----------
|
|
29
|
+
az : array_like
|
|
30
|
+
Azimuth angle in radians (0 = boresight, positive = right)
|
|
31
|
+
el : array_like
|
|
32
|
+
Elevation angle in radians (0 = horizon, positive = up)
|
|
33
|
+
|
|
34
|
+
Returns
|
|
35
|
+
-------
|
|
36
|
+
theta : array_like
|
|
37
|
+
Polar angle from z-axis in radians (0 = zenith)
|
|
38
|
+
phi : array_like
|
|
39
|
+
Azimuthal angle in radians
|
|
40
|
+
"""
|
|
41
|
+
az = np.asarray(az)
|
|
42
|
+
el = np.asarray(el)
|
|
43
|
+
|
|
44
|
+
theta = np.arccos(np.cos(az) * np.cos(el))
|
|
45
|
+
phi = np.arctan2(np.sin(az), np.tan(el))
|
|
46
|
+
|
|
47
|
+
return theta, phi
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def thetaphi_to_azel(theta: ArrayLike, phi: ArrayLike) -> Tuple[ArrayLike, ArrayLike]:
|
|
51
|
+
"""
|
|
52
|
+
Convert theta/phi (spherical coordinates) to azimuth/elevation.
|
|
53
|
+
|
|
54
|
+
Parameters
|
|
55
|
+
----------
|
|
56
|
+
theta : array_like
|
|
57
|
+
Polar angle from z-axis in radians (0 = zenith)
|
|
58
|
+
phi : array_like
|
|
59
|
+
Azimuthal angle in radians
|
|
60
|
+
|
|
61
|
+
Returns
|
|
62
|
+
-------
|
|
63
|
+
az : array_like
|
|
64
|
+
Azimuth angle in radians
|
|
65
|
+
el : array_like
|
|
66
|
+
Elevation angle in radians
|
|
67
|
+
"""
|
|
68
|
+
theta = np.asarray(theta)
|
|
69
|
+
phi = np.asarray(phi)
|
|
70
|
+
|
|
71
|
+
el = np.arcsin(np.cos(theta))
|
|
72
|
+
az = np.arctan2(np.sin(theta) * np.sin(phi), np.sin(theta) * np.cos(phi))
|
|
73
|
+
|
|
74
|
+
return az, el
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def theta_phi_to_uv(theta: ArrayLike, phi: ArrayLike) -> Tuple[ArrayLike, ArrayLike]:
|
|
78
|
+
"""
|
|
79
|
+
Convert theta/phi angles to UV-space (direction cosines).
|
|
80
|
+
|
|
81
|
+
Parameters
|
|
82
|
+
----------
|
|
83
|
+
theta : array_like
|
|
84
|
+
Polar angle from z-axis in radians
|
|
85
|
+
phi : array_like
|
|
86
|
+
Azimuthal angle in radians
|
|
87
|
+
|
|
88
|
+
Returns
|
|
89
|
+
-------
|
|
90
|
+
u : array_like
|
|
91
|
+
Direction cosine u = sin(theta) * cos(phi)
|
|
92
|
+
v : array_like
|
|
93
|
+
Direction cosine v = sin(theta) * sin(phi)
|
|
94
|
+
"""
|
|
95
|
+
theta = np.asarray(theta)
|
|
96
|
+
phi = np.asarray(phi)
|
|
97
|
+
|
|
98
|
+
u = np.sin(theta) * np.cos(phi)
|
|
99
|
+
v = np.sin(theta) * np.sin(phi)
|
|
100
|
+
|
|
101
|
+
return u, v
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def uv_to_theta_phi(u: ArrayLike, v: ArrayLike) -> Tuple[ArrayLike, ArrayLike]:
|
|
105
|
+
"""
|
|
106
|
+
Convert UV-space (direction cosines) to theta/phi angles.
|
|
107
|
+
|
|
108
|
+
Parameters
|
|
109
|
+
----------
|
|
110
|
+
u : array_like
|
|
111
|
+
Direction cosine u = sin(theta) * cos(phi)
|
|
112
|
+
v : array_like
|
|
113
|
+
Direction cosine v = sin(theta) * sin(phi)
|
|
114
|
+
|
|
115
|
+
Returns
|
|
116
|
+
-------
|
|
117
|
+
theta : array_like
|
|
118
|
+
Polar angle from z-axis in radians
|
|
119
|
+
phi : array_like
|
|
120
|
+
Azimuthal angle in radians
|
|
121
|
+
|
|
122
|
+
Notes
|
|
123
|
+
-----
|
|
124
|
+
Points outside the visible region (u^2 + v^2 > 1) will have
|
|
125
|
+
theta values computed from the magnitude, which may be complex
|
|
126
|
+
or undefined. Use is_visible_region() to check validity.
|
|
127
|
+
"""
|
|
128
|
+
u = np.asarray(u)
|
|
129
|
+
v = np.asarray(v)
|
|
130
|
+
|
|
131
|
+
r = np.sqrt(u**2 + v**2)
|
|
132
|
+
theta = np.arcsin(np.clip(r, -1, 1))
|
|
133
|
+
phi = np.arctan2(v, u)
|
|
134
|
+
|
|
135
|
+
return theta, phi
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def is_visible_region(u: ArrayLike, v: ArrayLike) -> np.ndarray:
|
|
139
|
+
"""
|
|
140
|
+
Check if UV coordinates are within the visible region.
|
|
141
|
+
|
|
142
|
+
Parameters
|
|
143
|
+
----------
|
|
144
|
+
u, v : array_like
|
|
145
|
+
Direction cosines
|
|
146
|
+
|
|
147
|
+
Returns
|
|
148
|
+
-------
|
|
149
|
+
visible : ndarray
|
|
150
|
+
Boolean array, True where u^2 + v^2 <= 1
|
|
151
|
+
"""
|
|
152
|
+
u = np.asarray(u)
|
|
153
|
+
v = np.asarray(v)
|
|
154
|
+
return (u**2 + v**2) <= 1.0
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def wavelength_to_k(wavelength: float) -> float:
|
|
158
|
+
"""
|
|
159
|
+
Convert wavelength to wavenumber.
|
|
160
|
+
|
|
161
|
+
Parameters
|
|
162
|
+
----------
|
|
163
|
+
wavelength : float
|
|
164
|
+
Wavelength in meters
|
|
165
|
+
|
|
166
|
+
Returns
|
|
167
|
+
-------
|
|
168
|
+
k : float
|
|
169
|
+
Wavenumber (2*pi/wavelength) in rad/m
|
|
170
|
+
"""
|
|
171
|
+
return 2.0 * np.pi / wavelength
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def frequency_to_wavelength(frequency: float, c: float = 3e8) -> float:
|
|
175
|
+
"""
|
|
176
|
+
Convert frequency to wavelength.
|
|
177
|
+
|
|
178
|
+
Parameters
|
|
179
|
+
----------
|
|
180
|
+
frequency : float
|
|
181
|
+
Frequency in Hz
|
|
182
|
+
c : float, optional
|
|
183
|
+
Speed of light in m/s (default: 3e8)
|
|
184
|
+
|
|
185
|
+
Returns
|
|
186
|
+
-------
|
|
187
|
+
wavelength : float
|
|
188
|
+
Wavelength in meters
|
|
189
|
+
"""
|
|
190
|
+
return c / frequency
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def frequency_to_k(frequency: float, c: float = 3e8) -> float:
|
|
194
|
+
"""
|
|
195
|
+
Convert frequency to wavenumber.
|
|
196
|
+
|
|
197
|
+
Parameters
|
|
198
|
+
----------
|
|
199
|
+
frequency : float
|
|
200
|
+
Frequency in Hz
|
|
201
|
+
c : float, optional
|
|
202
|
+
Speed of light in m/s (default: 3e8)
|
|
203
|
+
|
|
204
|
+
Returns
|
|
205
|
+
-------
|
|
206
|
+
k : float
|
|
207
|
+
Wavenumber in rad/m
|
|
208
|
+
"""
|
|
209
|
+
return 2.0 * np.pi * frequency / c
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def db_to_linear(db: ArrayLike) -> ArrayLike:
|
|
213
|
+
"""Convert decibels to linear scale (power)."""
|
|
214
|
+
return 10.0 ** (np.asarray(db) / 10.0)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def linear_to_db(linear: ArrayLike, min_db: float = -100.0) -> ArrayLike:
|
|
218
|
+
"""
|
|
219
|
+
Convert linear scale (power) to decibels.
|
|
220
|
+
|
|
221
|
+
Parameters
|
|
222
|
+
----------
|
|
223
|
+
linear : array_like
|
|
224
|
+
Linear power values
|
|
225
|
+
min_db : float, optional
|
|
226
|
+
Minimum dB value to return (clips zeros/negatives)
|
|
227
|
+
|
|
228
|
+
Returns
|
|
229
|
+
-------
|
|
230
|
+
db : array_like
|
|
231
|
+
Power in decibels
|
|
232
|
+
"""
|
|
233
|
+
linear = np.asarray(linear)
|
|
234
|
+
with np.errstate(divide='ignore', invalid='ignore'):
|
|
235
|
+
db = 10.0 * np.log10(linear)
|
|
236
|
+
db = np.where(np.isfinite(db), db, min_db)
|
|
237
|
+
return np.maximum(db, min_db)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def normalize_pattern(pattern: np.ndarray, mode: str = 'peak') -> np.ndarray:
|
|
241
|
+
"""
|
|
242
|
+
Normalize a radiation pattern.
|
|
243
|
+
|
|
244
|
+
Parameters
|
|
245
|
+
----------
|
|
246
|
+
pattern : ndarray
|
|
247
|
+
Complex or magnitude pattern values
|
|
248
|
+
mode : str
|
|
249
|
+
'peak' - normalize to peak value
|
|
250
|
+
'power' - normalize to total radiated power
|
|
251
|
+
|
|
252
|
+
Returns
|
|
253
|
+
-------
|
|
254
|
+
normalized : ndarray
|
|
255
|
+
Normalized pattern (same type as input)
|
|
256
|
+
"""
|
|
257
|
+
mag = np.abs(pattern)
|
|
258
|
+
|
|
259
|
+
if mode == 'peak':
|
|
260
|
+
max_val = np.max(mag)
|
|
261
|
+
if max_val > 0:
|
|
262
|
+
return pattern / max_val
|
|
263
|
+
return pattern
|
|
264
|
+
elif mode == 'power':
|
|
265
|
+
total_power = np.sum(mag**2)
|
|
266
|
+
if total_power > 0:
|
|
267
|
+
return pattern / np.sqrt(total_power)
|
|
268
|
+
return pattern
|
|
269
|
+
else:
|
|
270
|
+
raise ValueError(f"Unknown normalization mode: {mode}")
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def create_theta_phi_grid(
|
|
274
|
+
theta_range: Tuple[float, float] = (0, np.pi),
|
|
275
|
+
phi_range: Tuple[float, float] = (0, 2*np.pi),
|
|
276
|
+
n_theta: int = 181,
|
|
277
|
+
n_phi: int = 361
|
|
278
|
+
) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
|
|
279
|
+
"""
|
|
280
|
+
Create a theta/phi grid for pattern computation.
|
|
281
|
+
|
|
282
|
+
Parameters
|
|
283
|
+
----------
|
|
284
|
+
theta_range : tuple
|
|
285
|
+
(theta_min, theta_max) in radians
|
|
286
|
+
phi_range : tuple
|
|
287
|
+
(phi_min, phi_max) in radians
|
|
288
|
+
n_theta : int
|
|
289
|
+
Number of theta points
|
|
290
|
+
n_phi : int
|
|
291
|
+
Number of phi points
|
|
292
|
+
|
|
293
|
+
Returns
|
|
294
|
+
-------
|
|
295
|
+
theta_1d : ndarray
|
|
296
|
+
1D array of theta values
|
|
297
|
+
phi_1d : ndarray
|
|
298
|
+
1D array of phi values
|
|
299
|
+
theta_grid : ndarray
|
|
300
|
+
2D meshgrid of theta values
|
|
301
|
+
phi_grid : ndarray
|
|
302
|
+
2D meshgrid of phi values
|
|
303
|
+
"""
|
|
304
|
+
theta_1d = np.linspace(theta_range[0], theta_range[1], n_theta)
|
|
305
|
+
phi_1d = np.linspace(phi_range[0], phi_range[1], n_phi)
|
|
306
|
+
theta_grid, phi_grid = np.meshgrid(theta_1d, phi_1d, indexing='ij')
|
|
307
|
+
|
|
308
|
+
return theta_1d, phi_1d, theta_grid, phi_grid
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def create_uv_grid(
|
|
312
|
+
u_range: Tuple[float, float] = (-1, 1),
|
|
313
|
+
v_range: Tuple[float, float] = (-1, 1),
|
|
314
|
+
n_u: int = 201,
|
|
315
|
+
n_v: int = 201
|
|
316
|
+
) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
|
|
317
|
+
"""
|
|
318
|
+
Create a UV-space grid for pattern computation.
|
|
319
|
+
|
|
320
|
+
Parameters
|
|
321
|
+
----------
|
|
322
|
+
u_range : tuple
|
|
323
|
+
(u_min, u_max) direction cosine range
|
|
324
|
+
v_range : tuple
|
|
325
|
+
(v_min, v_max) direction cosine range
|
|
326
|
+
n_u : int
|
|
327
|
+
Number of u points
|
|
328
|
+
n_v : int
|
|
329
|
+
Number of v points
|
|
330
|
+
|
|
331
|
+
Returns
|
|
332
|
+
-------
|
|
333
|
+
u_1d : ndarray
|
|
334
|
+
1D array of u values
|
|
335
|
+
v_1d : ndarray
|
|
336
|
+
1D array of v values
|
|
337
|
+
u_grid : ndarray
|
|
338
|
+
2D meshgrid of u values
|
|
339
|
+
v_grid : ndarray
|
|
340
|
+
2D meshgrid of v values
|
|
341
|
+
"""
|
|
342
|
+
u_1d = np.linspace(u_range[0], u_range[1], n_u)
|
|
343
|
+
v_1d = np.linspace(v_range[0], v_range[1], n_v)
|
|
344
|
+
u_grid, v_grid = np.meshgrid(u_1d, v_1d, indexing='ij')
|
|
345
|
+
|
|
346
|
+
return u_1d, v_1d, u_grid, v_grid
|