pueoAnalysisTools
Loading...
Searching...
No Matches
time_delays.py
Go to the documentation of this file.
1r"""!
2@file time_delays.py
3@brief `main()` shows how to use
4[make_time_delay_skymap()](#time_delays.make_time_delay_skymap)
5
6* Running the `main` function through `python3 time_delays.py` will print out a DataFrame
7 containing one pair of antennas (`Ant 203` and `Ant 104`)
8
9* The dataframe will also have a column `time delays [sec]`, which is the "time delay skymap"
10 (for this pair of antennas).
11
12* [_plot_one_random_row_as_example()](#time_delays._plot_one_random_row_as_example) will plot the
13 time delay skymap for a randomly selected pair of antennas.
14 * \f$\phi\f$ denotes the azimuthal angle of the incoming signal's direction, and
15 * \f$\theta\f$ the zenith angle (in payload coordinates).
16 * time (the colorbar) in seconds.
17
18![Time Delays of Signals from Different Directions](time_delay_map_in_seconds.svg)
19"""
20
21import polars as pl
22import numpy as np
23
24
28
29
30def _make_grid(sparse=False) -> tuple[np.ndarray, np.ndarray]:
31 r"""!
32 @ingroup delay
33 @brief Creates a 2D grid \f$(\phi, \theta)\f$ of azimuthal and zenith angles.
34 @param[in] sparse see [NumPy's documentation](https://numpy.org/doc/2.2/reference/generated/numpy.meshgrid.html#numpy-meshgrid)
35 @retval phi azimuthal angle coordinates
36 @retval theta zenith angle coordinates
37
38 * This grid represents the "bottom hemisphere":
39 * Azimuthal angle range:\f$0 < \phi < 2 \pi\f$
40 * **Zenith** (not elevation!) angle range: \f$ \pi/2 < \theta < \pi\f$
41 * The figure below shows a sparse grid as an example.
42 @note The real grid currently in use is 180-by-360
43 (i.e. 360 points for \f$\phi\f$, 180 points for \f$\theta\f$)
44
45 ![Example 30-by-3 grid](southern_hemisphere.svg)
46 """
47 azimuthal_angle_mesh = np.linspace(0, 2 * np.pi, 360)
48 zenith_angle_mesh = np.linspace(np.pi / 2, np.pi, 180)
49 phi, theta = np.meshgrid(azimuthal_angle_mesh, zenith_angle_mesh, sparse=sparse)
50
51 return phi, theta
52
53
54def _make_unit_vectors_on_bottom_hemisphere() -> tuple[np.ndarray, np.ndarray, np.ndarray]:
55 r"""!
56 @ingroup delay
57 @brief Returns unit vectors pointing to the origin from the surface of a unit bottom hemisphere.
58 @retval incmoing_x
59 @retval incmoing_y
60 @retval incmoing_z
61
62 * Calls #_make_grid() to generate a 2D grid of \f$\phi\f$s and \f$\theta\f$s.
63 * Converts each point on the grid into Cartesian coordinates; assuming `r=1`:
64 * \f$ x = \sin\theta \cdot \cos\phi \f$
65 * \f$ y = \sin\theta \cdot \sin\phi \f$
66 * \f$ z = \cos\theta \f$
67 * Each point is a unit vector pointing from the origin to somewhere on the hemisphere.
68 * Thus, the unit vector \f$\hat{\bf{u}}\equiv(-x,-y,-z)\f$ represents a possible (incoming) signal
69 direction, assuming that the payload is at the center of the sphere.
70 * The unit bottom hemisphere:
71 ```python
72 incoming_x, incoming_y, incoming_z = _make_unit_vectors_on_bottom_hemisphere()
73 fig = plt.figure()
74 ax = fig.add_subplot(projection='3d')
75 ax.plot_surface(-incoming_x, -incoming_y, -incoming_z)
76 ax.set_aspect('equal')
77 ```
78
79 ![A Unit "Southern" Hemisphere](southern_hemisphere_3D.svg)
80 """
81 phi, theta = _make_grid()
82 incoming_x = -np.sin(theta) * np.cos(phi)
83 incoming_y = -np.sin(theta) * np.sin(phi)
84 incoming_z = -np.cos(theta)
85
86 return incoming_x, incoming_y, incoming_z
87
88
89def make_time_delay_skymap(antenna_pairs: pl.DataFrame) -> pl.DataFrame:
90 r"""! Computes the expected incoming-signal time delays between
91 [antenna pairs](#antenna_pairs.generate_MI_antenna_pairs)
92 based on pairwise displacement vectors and signal directions.
93 @ingroup delay
94 @param[in] antenna_pairs The output of #antenna_pairs.generate_MI_antenna_pairs
95 @retval time_delays See sample output below
96
97* The following columns are **required** in `antenna_pairs`
98 * `A1_X[m]`
99 * `A1_Y[m]`
100 * `A1_Z[m]`
101 * `A2_X[m]`
102 * `A2_Y[m]`
103 * `A2_Z[m]`
104
105* Sample schema of `time_delays`, assuming only the essential columns have been supplied in `antenna_pairs`
106```
107$ A1_X[m] <f64>
108$ A1_Y[m] <f64>
109$ A1_Z[m] <f64>
110$ A2_X[m] <f64>
111$ A2_Y[m] <f64>
112$ A2_Z[m] <f64>
113$ time delays [sec] <array[f64, (180, 360)]>
114```
115* Explanation:
116 -# The time delay between any antenna pair is computed via
117 \f$ (\bf{X_1} - \bf{X_2}) \cdot \hat{\bf{u}} \f$ / \f$c\f$
118 * \f$\bf{X}\f$ denotes the antenna position,
119 * \f$\hat{\bf{u}}\f$ is a unit vector that represents the
120 [signal direction](#_make_unit_vectors_on_bottom_hemisphere), and
121 * \f$c\f$ is the speed of light.
122
123 -# Each entry in the column `time delays [sec]` would be a 2D matrix with the same dimensions
124 as the 2D grid generated by #_make_grid; let's call them **time delay skymaps**.
125 -# Each point on a time delay skymap represents the expected time-delay based on where the
126 signal is coming from and where the antennas are on the payload.
127 -# Time delay occurs since the signal has to travel an extra distance to reach the second antenna,
128 after it hits the first antenna:
129 \anchor timedelaybydotproduct
130 \image html time_delay_by_dot_product.png width=50%
131
132 -# As an example, the figure below shows the expected time delays (in seconds) between antenna
133 203 and 104.
134
135 ![Time Delays of Signals from Different Directions](time_delay_map_in_seconds.svg)
136 """
137 from scipy import constants
138 incoming_x, incoming_y, incoming_z = _make_unit_vectors_on_bottom_hemisphere()
139
140 # Compute the displacements between antenna pairs
141 time_delays = (
142 antenna_pairs
143 .with_columns(
144 pl.concat_arr("A1_X[m]", "A1_Y[m]", "A1_Z[m]").alias("A1_position[m]"),
145 pl.concat_arr("A2_X[m]", "A2_Y[m]", "A2_Z[m]").alias("A2_position[m]")
146 )
147 .with_columns( # dot product between the antenna displacement vector and signal direction
148 (pl.col("A1_position[m]") - pl.col("A2_position[m]")).map_batches(
149 lambda displacement_vector: np.array([
150 (dx * incoming_x + dy * incoming_y + dz * incoming_z) / constants.c
151 for (dx, dy, dz) in displacement_vector
152 ])
153 ).alias("time delays [sec]")
154 )
155 .select(pl.all().exclude(r"^A[12]_position\[m\]$"))
156 )
157
158 return time_delays
159
160
161def _plot_one_random_row_as_example(one_row: pl.DataFrame, plot_name: str) -> None:
162 """!Makes an example plot"""
163 import matplotlib.pyplot as plt
164 plt.rcParams.update({"font.size": 16})
165
166 pl.Config.set_tbl_cols(one_row.width)
167 print(one_row)
168 phi, theta = _make_grid()
169 plt.contourf(phi, theta, one_row["time delays [sec]"][0])
170 plt.axis('scaled')
171 plt.xlabel(r"$\phi$")
172 plt.ylabel(r"$\theta$")
173 plt.xticks([0, np.pi, 2 * np.pi], labels=["0", r"$\pi$", r"$2\pi$"])
174 plt.yticks([np.pi / 2, np.pi], labels=[r"$\pi / 2$", r"$\pi$"])
175 plt.colorbar(label="time delay [sec]")
176 plt.savefig(plot_name)
177
178
179if __name__ == "__main__":
180
181 import os
182 from pathlib import Path
183 from antenna_attributes import read_MI_antenna_geometry
184 from antenna_pairs import generate_MI_antenna_pairs
185
186 uncalibrated_antenna_positions: Path = (
187 os.environ.get("PUEO_UTIL_INSTALL_DIR") / Path("share/pueo/geometry/jun25/qrh.dat")
188 )
189 antennas: pl.DataFrame = read_MI_antenna_geometry(uncalibrated_antenna_positions)
190
191 antenna_pairs = generate_MI_antenna_pairs(antennas)
192
193 try:
194 bad = antenna_pairs.select(pl.all().exclude(r"^A[12]_[XYZ]\[m\]$"))
195 time_delay = make_time_delay_skymap(bad)
196 except pl.exceptions.ColumnNotFoundError:
197 print("The above should fail due to missing columns!")
198
199 print("But below is okay and the output is:")
200 okay = antenna_pairs.select(r"^A[12]_[XYZ]\[m\]$")
201 time_delay = make_time_delay_skymap(okay)
202
203 _plot_one_random_row_as_example(time_delay.sample(1), plot_name="foobar")