pyant 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.
clibbeam/Makefile ADDED
@@ -0,0 +1,26 @@
1
+ CC=gcc
2
+ CFLAGS=-fPIC
3
+ LIBS=-lm
4
+
5
+ SOURCES=$(wildcard *.c)
6
+ OBJECTS=$(SOURCES:.c=.o)
7
+ EXT=$(shell python -c "import sysconfig; print(sysconfig.get_config_var('EXT_SUFFIX'))")
8
+ OUTLIB=clibbeam$(EXT)
9
+
10
+ all: $(OUTLIB)
11
+
12
+ %.o: %.c
13
+ @echo "pyant installation -> Compiling source file $< ..."
14
+ $(CC) -c $(CFLAGS) -o $@ $<
15
+
16
+ $(OUTLIB): $(OBJECTS)
17
+ @echo "pyant installation -> Linking shared library $@ ..."
18
+ $(CC) $(CFLAGS) -shared $(OBJECTS) $(LIBS) -o $@
19
+ @mv $@ ../pyant/ -v
20
+ @echo "pyant installation -> The shared library $< has been created successfully."
21
+
22
+ clean:
23
+ @echo "pyant installation -> Removing object files *.o ..."
24
+ @-rm -f *.o
25
+ @echo "pyant installation -> Removing shared library *.so ..."
26
+ @-rm -f *.so
clibbeam/array.c ADDED
@@ -0,0 +1,18 @@
1
+ #include <stdio.h>
2
+ #include <complex.h>
3
+ #include <math.h>
4
+ #include "beam.h"
5
+
6
+ // todo: test if this is actually faster or not
7
+ // do we really need this??
8
+ void
9
+ array_sensor_response(
10
+ double *k,
11
+ double complex *G,
12
+ size_t channels
13
+ )
14
+ {
15
+ for(size_t ch = 0; ch < channels; ch++) {
16
+ G[ch] = 1.0;
17
+ }
18
+ }
clibbeam/beam.c ADDED
@@ -0,0 +1,20 @@
1
+ #include <stdio.h>
2
+ #include <complex.h>
3
+ #include <math.h>
4
+ #include "beam.h"
5
+
6
+ int
7
+ main(int argc, char *argv[])
8
+ {
9
+ double kvec[3];
10
+ size_t channels = 1;
11
+ double complex G[channels];
12
+ kvec[0] = 0.0;
13
+ kvec[1] = 0.0;
14
+ kvec[2] = 1.0;
15
+ array_sensor_response(kvec, G, channels);
16
+
17
+ printf("Hello, G = %.2f + %.2fi\n", creal(G[0]), cimag(G[0]));
18
+
19
+ return 0;
20
+ }
clibbeam/beam.h ADDED
@@ -0,0 +1,11 @@
1
+ // inclusion guard
2
+ #ifndef BEAM_H_
3
+ #define BEAM_H_
4
+
5
+ void
6
+ array_sensor_response(
7
+ double *k,
8
+ double complex *G,
9
+ size_t channels
10
+ );
11
+ #endif // BEAM_H_
pyant/__init__.py ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env python
2
+
3
+ """ """
4
+ import types
5
+ import importlib
6
+ from .version import __version__
7
+
8
+ # Modules
9
+ if importlib.util.find_spec("matplotlib") is not None:
10
+ from . import plotting
11
+ else:
12
+
13
+ class _MissingModule(types.ModuleType):
14
+ def __getattr__(self, name):
15
+ raise ImportError(
16
+ "The optional dependency `matplotlib` for 'plotting' module is missing.\n"
17
+ "Install it with `pip install pyant[plotting]` or `pip install matplotlib`."
18
+ )
19
+
20
+ plotting = _MissingModule("plotting")
21
+
22
+ from . import coordinates
23
+ from . import models
24
+ from . import statistics
25
+ from . import beams
26
+
27
+ # from . import clib
28
+
29
+ from .beam import Beam
pyant/beam.py ADDED
@@ -0,0 +1,301 @@
1
+ #!/usr/bin/env python
2
+
3
+ """Defines an antenna's or entire radar system's radiation pattern"""
4
+ from abc import ABC, abstractmethod
5
+ import collections
6
+
7
+ import numpy as np
8
+ from numpy.typing import NDArray
9
+ import scipy.constants
10
+
11
+ from . import coordinates
12
+
13
+
14
+ class Beam(ABC):
15
+ """Defines the radiation pattern, i.e gain, of a radar. Gain here means
16
+ amplification of the electromagnetic wave amplitude when transferred to a
17
+ complex voltage amplitude.
18
+
19
+ Notes
20
+ ------
21
+ There are four possible ways of broadcasting input arrays over the parameters:
22
+
23
+ The additional axis of all parameters lines up with the input wave vectors additional
24
+ axis size, in which case each input k-vector gets evaluated versus each set of
25
+ parameters.
26
+ Input size (3, n), parameter shapes (..., n), output size (n,).
27
+
28
+ The parameters are all scalars and the input k-vector gets evaluated over this single set.
29
+ Input size (3, n), parameter shapes (...), output size (n,).
30
+
31
+ The additional axis of all parameters line up and the input k-vector is a single vector,
32
+ in which case this vector gets computed for all sets of parameters.
33
+ Input size (3,), parameter shapes (..., n), output size (n,).
34
+
35
+ The parameters are all scalars and the input k-vector is a single vector.
36
+ Input size (3,), parameter shapes (...), output size ().
37
+
38
+ These ways allow for any set of computations and broadcasts (although they need to be prepared
39
+ outside the scope of this class) to be set-up using the Beam interface.
40
+
41
+ """
42
+
43
+ def __init__(self):
44
+ """Basic constructor."""
45
+ self.parameters = collections.OrderedDict()
46
+ self.parameters_shape = {}
47
+
48
+ def _get_parameter_len(self, key: str):
49
+ """Get the length of a parameter axis, its always the last array dimension"""
50
+ obj = self.parameters[key]
51
+ if isinstance(obj, np.ndarray):
52
+ if key in self.parameters_shape:
53
+ shape = self.parameters_shape[key]
54
+ if len(obj.shape) == len(shape):
55
+ return 0
56
+ else:
57
+ return obj.shape[-1]
58
+ else:
59
+ return obj.shape[-1]
60
+ else:
61
+ return 0
62
+
63
+ @property
64
+ def size(self):
65
+ """The additional dimensions added to the output Gain if broadcasting is enabled."""
66
+ shape = [self._get_parameter_len(key) for key in self.parameters]
67
+ if len(shape) == 0:
68
+ return 0
69
+ assert all(x == shape[0] for x in shape), (
70
+ "all parameter shapes must line up:"
71
+ f"{list(self.parameters.keys())} -> {shape}"
72
+ )
73
+ return shape[0]
74
+
75
+ def validate_parameter_shapes(self):
76
+ """Helper function to validate the input parameter shapes are correct"""
77
+ # TODO: maybe change to raise custom exceptions?
78
+ size = None
79
+ for key, p in self.parameters.items():
80
+ if size is None:
81
+ size = self._get_parameter_len(key)
82
+ assert size == self._get_parameter_len(key), "all parameter shapes must line up"
83
+ if key in self.parameters_shape:
84
+ shape = self.parameters_shape[key]
85
+ assert len(p.shape) <= len(shape) + 1 and len(p.shape) >= len(
86
+ shape
87
+ ), f"{key} can only have {len(shape)} or {len(shape) + 1} axis, not {len(p.shape)}"
88
+ assert (
89
+ p.shape[: len(shape)] == shape
90
+ ), f"{key} needs at least {shape} dimensions, not {p.shape}"
91
+
92
+ def validate_k_shape(self, k):
93
+ """Helper function to validate the input direction vector shape is correct"""
94
+ # TODO: maybe change to raise custom exceptions?
95
+ size = self.size
96
+ k_len = k.shape[1] if len(k.shape) > 1 else 0
97
+ if size > 0:
98
+ assert (
99
+ size == k_len or k_len == 0
100
+ ), "input k vector must either be single vector or line up with parameter dimensions"
101
+ assert len(k.shape) <= 2, "k vector can only be vectorized along one extra axis"
102
+ assert k.shape[0] == 3, f"pointing vector must at least be a 3-vector, not {k.shape[0]}"
103
+ return k_len
104
+
105
+ @property
106
+ def keys(self):
107
+ """Current list of parameters."""
108
+ return self.parameters.keys()
109
+
110
+ @staticmethod
111
+ def _azel_to_numpy(azimuth: NDArray | float, elevation: NDArray | float) -> NDArray:
112
+ """Convert input azimuth and elevation to spherical coordinates states,
113
+ i.e a `shape=(3,n)` numpy array.
114
+ """
115
+
116
+ az_len = azimuth.size if isinstance(azimuth, np.ndarray) else None
117
+ el_len = elevation.size if isinstance(elevation, np.ndarray) else None
118
+
119
+ if el_len is not None and az_len is not None:
120
+ assert el_len == az_len, f"azimuth {az_len} and elevation {el_len} sizes must agree"
121
+
122
+ if az_len is not None:
123
+ shape = (3, az_len)
124
+ elif el_len is not None:
125
+ shape = (3, el_len)
126
+ else:
127
+ shape = (3, )
128
+
129
+ sph = np.empty(shape, dtype=np.float64)
130
+ sph[0, ...] = azimuth
131
+ sph[1, ...] = elevation
132
+ sph[2, ...] = 1.0
133
+
134
+ return sph
135
+
136
+ def copy(self):
137
+ """Return a copy of the current instance."""
138
+ raise NotImplementedError("")
139
+
140
+ def to_h5(self, path):
141
+ """Write defining parameters to a h5 file"""
142
+ raise NotImplementedError("")
143
+
144
+ @classmethod
145
+ def from_h5(cls):
146
+ """Load defining parameters from a h5 file and instantiate a beam"""
147
+ raise NotImplementedError("")
148
+
149
+ @property
150
+ def frequency(self):
151
+ """The radar wavelength."""
152
+ return self.parameters["frequency"]
153
+
154
+ @frequency.setter
155
+ def frequency(self, val):
156
+ self.parameters["frequency"] = val
157
+
158
+ @property
159
+ def wavelength(self):
160
+ """The radar wavelength."""
161
+ return scipy.constants.c / self.frequency
162
+
163
+ @wavelength.setter
164
+ def wavelength(self, val):
165
+ self.frequency = scipy.constants.c / val
166
+
167
+ def sph_point(
168
+ self, azimuth: NDArray | float, elevation: NDArray | float, degrees: bool = False
169
+ ):
170
+ """Point beam towards azimuth and elevation coordinate.
171
+
172
+ Parameters
173
+ ----------
174
+ azimuth : float
175
+ Azimuth east of north of pointing direction.
176
+ elevation : float
177
+ Elevation from horizon of pointing direction.
178
+ degrees : bool
179
+ If :code:`True` all input/output angles are in degrees,
180
+ else they are in radians. Defaults to instance
181
+ settings :code:`self.radians`.
182
+
183
+ """
184
+ sph = Beam._azel_to_numpy(azimuth, elevation)
185
+ self.parameters["pointing"] = coordinates.sph_to_cart(sph, degrees=degrees)
186
+
187
+ def point(self, k: NDArray):
188
+ """Point beam in local Cartesian direction.
189
+
190
+ Parameters
191
+ ----------
192
+ k : numpy.ndarray
193
+ Pointing direction in local coordinates.
194
+
195
+ """
196
+ self.parameters["pointing"] = k / np.linalg.norm(k, axis=0)
197
+
198
+ def sph_angle(
199
+ self, azimuth: NDArray | float, elevation: NDArray | float, degrees: bool = False
200
+ ) -> NDArray | float:
201
+ """Get angle between azimuth and elevation and pointing direction.
202
+
203
+ Parameters
204
+ ----------
205
+ azimuth : float or NDArray
206
+ Azimuth east of north of pointing direction.
207
+ elevation : float or NDArray
208
+ Elevation from horizon of pointing direction.
209
+ degrees : bool
210
+ If :code:`True` all input/output angles are in degrees,
211
+ else they are in radians.
212
+
213
+ Returns
214
+ -------
215
+ float or NDArray
216
+ Angle between pointing and given direction.
217
+
218
+ """
219
+ sph = Beam._azel_to_numpy(azimuth, elevation)
220
+ k = coordinates.sph_to_cart(sph, degrees=degrees)
221
+ return self.angle(k, degrees=degrees)
222
+
223
+ def angle(self, k: NDArray, degrees: bool = False) -> NDArray | float:
224
+ """Get angle between local direction and pointing direction.
225
+
226
+ Parameters
227
+ ----------
228
+ k : numpy.ndarray
229
+ Direction to evaluate angle to.
230
+ degrees : bool
231
+ If :code:`True` all input/output angles are in degrees,
232
+ else they are in radians. Defaults to instance
233
+ settings :code:`self.radians`.
234
+
235
+ Returns
236
+ -------
237
+ float or NDArray
238
+ Angle between pointing and given direction.
239
+
240
+ """
241
+ pt: NDArray = self.parameters["pointing"]
242
+ return coordinates.vector_angle(pt, k, degrees=degrees)
243
+
244
+ @abstractmethod
245
+ def gain(self, k: NDArray, polarization: NDArray | None = None):
246
+ """Return the gain in the given direction. This method should be
247
+ vectorized in the `k` variable.
248
+
249
+ Parameters
250
+ ----------
251
+ k : numpy.ndarray
252
+ Direction in local coordinates to evaluate
253
+ gain in. Must be a `(3,)` vector or a `(3,n)` matrix.
254
+ polarization : numpy.ndarray
255
+ The Jones vector of the incoming
256
+ plane waves, if applicable for the beam in question.
257
+
258
+ Returns
259
+ -------
260
+ float/numpy.ndarray
261
+ Radar gain in the given direction. If input is a `(3,)`
262
+ vector, output is a float. If input is a `(3,n)` matrix output
263
+ is a `(n,)` vector of gains.
264
+
265
+ """
266
+ pass
267
+
268
+ def sph_gain(
269
+ self,
270
+ azimuth: NDArray | float,
271
+ elevation: NDArray | float,
272
+ polarization: NDArray | None = None,
273
+ degrees: bool = False,
274
+ ):
275
+ """Return the gain in the given direction.
276
+
277
+ Parameters
278
+ ----------
279
+ azimuth : float
280
+ Azimuth east of north to evaluate gain in.
281
+ elevation : float
282
+ Elevation from horizon to evaluate gain in.
283
+ degrees : bool
284
+ If :code:`True` all input/output angles are in degrees,
285
+ else they are in radians. Defaults to instance
286
+ settings :code:`self.radians`.
287
+
288
+ Returns
289
+ -------
290
+ float/numpy.ndarray
291
+ Radar gain in the given direction. If input is a `(3,)`
292
+ vector, output is a float. If input is a `(3,n)` matrix output
293
+ is a `(n,)` vector of gains.
294
+ """
295
+ sph = Beam._azel_to_numpy(azimuth, elevation)
296
+
297
+ k = coordinates.sph_to_cart(sph, degrees=degrees)
298
+ return self.gain(
299
+ k,
300
+ polarization=polarization,
301
+ )
pyant/beam_fitting.py ADDED
@@ -0,0 +1,2 @@
1
+ # todo this should be able to fit the models to data!
2
+ # todo we need a persistence for beams too
@@ -0,0 +1,7 @@
1
+ """
2
+ Subpackage of functions that generate particular types of beam configurations, e.g. typical antenna
3
+ array layouts and such.
4
+
5
+ """
6
+
7
+ from .arrays import equidistant_archimedian_spiral
pyant/beams/arrays.py ADDED
@@ -0,0 +1,29 @@
1
+ import numpy as np
2
+
3
+ from ..models import Array
4
+ from .. import coordinates
5
+
6
+
7
+ def equidistant_archimedian_spiral(
8
+ antenna_num,
9
+ arc_separation,
10
+ range_coefficient,
11
+ frequency,
12
+ pointing,
13
+ degrees=True,
14
+ ):
15
+ # https://math.stackexchange.com/a/2216736
16
+ antennas = np.zeros((3, antenna_num))
17
+ for ind in range(1, antenna_num):
18
+ d_theta = arc_separation / np.sqrt(1 + antennas[0, ind - 1] ** 2)
19
+ antennas[0, ind] = antennas[0, ind - 1] + d_theta
20
+ antennas[2, ind] = range_coefficient * antennas[0, ind]
21
+
22
+ antennas = coordinates.sph_to_cart(antennas, degrees=False)
23
+ antennas = antennas.reshape((3, 1, antenna_num))
24
+
25
+ return Array(
26
+ pointing=pointing,
27
+ frequency=frequency,
28
+ antennas=antennas,
29
+ )
pyant/clib.py ADDED
@@ -0,0 +1,17 @@
1
+ """python-interface to c-library beam
2
+ """
3
+ import sysconfig
4
+ import pathlib
5
+ import ctypes
6
+
7
+ # Load the C-lib
8
+ suffix = sysconfig.get_config_var("EXT_SUFFIX")
9
+ if suffix is None:
10
+ suffix = ".so"
11
+
12
+ # We start by making a path to the current directory.
13
+ pymodule_dir = pathlib.Path(__file__).resolve().parent
14
+ __libpath__ = pymodule_dir / ("clibbeam" + suffix)
15
+
16
+ # Then we open the created shared clibbeam file
17
+ clib_beam = ctypes.cdll.LoadLibrary(__libpath__)