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/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