pueoAnalysisTools
Loading...
Searching...
No Matches
antenna_attributes.py
Go to the documentation of this file.
1"""!
2@file antenna_attributes.py
3@brief `main()` shows how to use
4[read_MI_antenna_geometry()](#antenna_attributes.read_MI_antenna_geometry) and
5[read_MI_antenna_geometry()](#antenna_attributes.read_global_channel_mapping).
6"""
7
8from pathlib import Path
9import polars as pl
10import os
11
12
15
16
29ALL_PUEO_ANTENNA_NAMES = (
30 pl.Enum([
31 "101", "201", "301", "401",
32 "102", "202", "302", "402",
33 "103", "203", "303", "403",
34 "104", "204", "304", "404",
35 "105", "205", "305", "405",
36 "106", "206", "306", "406",
37 "107", "207", "307", "407",
38 "108", "208", "308", "408",
39 "109", "209", "309", "409",
40 "110", "210", "310", "410",
41 "111", "211", "311", "411",
42 "112", "212", "312", "412",
43 "113", "213", "313", "413",
44 "114", "214", "314", "414",
45 "115", "215", "315", "415",
46 "116", "216", "316", "416",
47 "117", "217", "317", "417",
48 "118", "218", "318", "418",
49 "119", "219", "319", "419",
50 "120", "220", "320", "420",
51 "121", "221", "321", "421",
52 "122", "222", "322", "422",
53 "123", "223", "323", "423",
54 "124", "224", "324", "424",
55 "501", "505",
56 "605", "609",
57 "713",
58 "813", "817", "821"
59 ])
60)
61
62
68ANTENNA_POLARIZATION_TYPE = pl.Enum(["H", "V"])
69
70
73PUEO_ANTENNA_TYPE = pl.Enum(["MI", "LF"])
74
75
81NUM_PHI_SECTORS = 24
82
83
91ASSUMED_PHI_SECTOR_APERTURE_WIDTH = 50
92
93
94def read_global_channel_mapping() -> pl.DataFrame:
95 """!
96 @ingroup AAA
97 @brief Generates a Polars DataFrame from
98 [map.dat](https://github.com/PUEOCollaboration/pueo-data/blob/main/geometry/jun25/map.dat)
99 in the pueo-data repository.
100
101 * `map.dat` contains the channel mapping between the indices `GlobalChan`, `AntNum` and `AntIdx`
102 for all channels
103 * That is, the dataframe contains both polarizations (V and H) of both instruments (MI and LF).
104 * `GlobalChan` is the index used by the `pueoEvent` repository to access the waveforms,
105 see its [example script](https://github.com/PUEOCollaboration/pueoEvent/tree/main/examples).
106 * The relevant line is
107 ```
108 chan = pueo.GeomTool.Instance().getChanIndexFromRingPhiPol(
109 pueo.ring.ring_t.kTopRing, 10, pueo.pol.pol_t.kVertical
110 )
111 ```
112 * `chan` in the line above comes from the column `GlobalChan` in the DataFrame.
113 * Sample output:
114```
115shape: (208, 10)
116┌────────────┬────────┬────────┬──────┬─────────┬─────────┬─────────────┬───────────┬──────┬─────────┐
117│ GlobalChan ┆ AntNum ┆ AntIdx ┆ Pol ┆ AntType ┆ SurfNum ┆ SurfChanNum ┆ PhiSector ┆ Ring ┆ Comment │
118│ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │
119│ u32 ┆ enum ┆ u32 ┆ enum ┆ enum ┆ u8 ┆ u8 ┆ u8 ┆ u8 ┆ str │
120╞════════════╪════════╪════════╪══════╪═════════╪═════════╪═════════════╪═══════════╪══════╪═════════╡
121│ 0 ┆ 101 ┆ 0 ┆ H ┆ MI ┆ 0 ┆ 0 ┆ 1 ┆ 1 ┆ null │
122│ 1 ┆ 201 ┆ 1 ┆ H ┆ MI ┆ 0 ┆ 1 ┆ 1 ┆ 2 ┆ null │
123│ 2 ┆ 301 ┆ 2 ┆ H ┆ MI ┆ 0 ┆ 2 ┆ 1 ┆ 3 ┆ null │
124│ 3 ┆ 401 ┆ 3 ┆ H ┆ MI ┆ 0 ┆ 3 ┆ 1 ┆ 4 ┆ null │
125│ 4 ┆ 102 ┆ 4 ┆ H ┆ MI ┆ 0 ┆ 4 ┆ 2 ┆ 1 ┆ null │
126│ … ┆ … ┆ … ┆ … ┆ … ┆ … ┆ … ┆ … ┆ … ┆ … │
127│ 203 ┆ 609 ┆ 99 ┆ V ┆ LF ┆ 25 ┆ 3 ┆ 9 ┆ 6 ┆ null │
128│ 204 ┆ 713 ┆ 100 ┆ V ┆ LF ┆ 25 ┆ 4 ┆ 13 ┆ 7 ┆ null │
129│ 205 ┆ 813 ┆ 101 ┆ V ┆ LF ┆ 25 ┆ 5 ┆ 13 ┆ 8 ┆ null │
130│ 206 ┆ 817 ┆ 102 ┆ V ┆ LF ┆ 25 ┆ 6 ┆ 17 ┆ 8 ┆ null │
131│ 207 ┆ 821 ┆ 103 ┆ V ┆ LF ┆ 25 ┆ 7 ┆ 21 ┆ 8 ┆ null │
132└────────────┴────────┴────────┴──────┴─────────┴─────────┴─────────────┴───────────┴──────┴─────────┘
133```
134 """
135
136 map_dot_dat_file = os.environ.get("PUEO_UTIL_INSTALL_DIR") / Path("share/pueo/geometry/jun25/map.dat")
137 all_channels: pl.DataFrame = (
138 pl.read_csv(
139 map_dot_dat_file,
140 comment_prefix="#",
141 schema_overrides={
142 "GlobalChan": pl.UInt32,
143 "AntIdx": pl.UInt32,
144 "Pol": ANTENNA_POLARIZATION_TYPE,
145 "AntType": PUEO_ANTENNA_TYPE,
146 "SurfNum": pl.UInt8,
147 "SurfChanNum": pl.UInt8,
148 "PhiSector": pl.UInt8,
149 "Ring": pl.UInt8
150 }
151 )
152 .with_columns(
153 pl.col("AntNum").cast(pl.String).cast(ALL_PUEO_ANTENNA_NAMES)
154 )
155 )
156
157 return all_channels
158
159
160def read_MI_antenna_geometry(qrh_dot_dat: Path) -> pl.DataFrame:
161 """!
162 @ingroup AAA
163 @brief Reads in antenna positions (Cartesian coordinates, meters) and attitudes (degrees) from
164 [qrh.dat](https://github.com/PUEOCollaboration/pueo-data/blob/main/geometry/jun25/qrh.dat)
165 in the pueo-data repository.
166 @param[in] qrh_dot_dat Path to `qrh.dat`
167
168 * Note that `qrh.dat` contains the **face centers** of all main-instrument (MI) antennas
169 (quad-ridged horn antennas) and NOT the **phase center**.
170
171 * The function will error out if, although unlikely, `qrh.dat` contains an
172 index that is not listed in #ALL_PUEO_ANTENNA_NAMES.
173
174 * Sample output:
175 ```
176shape: (96, 10)
177┌────────┬──────┬───────────┬────────┬───────┬────────┬───────┬────────────┬──────────┬───────────┐
178│ AntNum ┆ Ring ┆ PhiSector ┆ AntIdx ┆ X[m] ┆ Y[m] ┆ Z[m] ┆ Pitch[deg] ┆ Yaw[deg] ┆ Roll[deg] │
179│ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │
180│ enum ┆ u8 ┆ u8 ┆ u32 ┆ f64 ┆ f64 ┆ f64 ┆ f64 ┆ f64 ┆ f64 │
181╞════════╪══════╪═══════════╪════════╪═══════╪════════╪═══════╪════════════╪══════════╪═══════════╡
182│ 101 ┆ 1 ┆ 1 ┆ 0 ┆ 1.488 ┆ 0.0 ┆ 5.114 ┆ -10.0 ┆ 0.0 ┆ 0.0 │
183│ 201 ┆ 2 ┆ 1 ┆ 1 ┆ 2.596 ┆ 0.0 ┆ 1.457 ┆ -10.0 ┆ 0.0 ┆ 0.0 │
184│ 301 ┆ 3 ┆ 1 ┆ 2 ┆ 2.596 ┆ 0.0 ┆ 0.728 ┆ -10.0 ┆ 0.0 ┆ 0.0 │
185│ 401 ┆ 4 ┆ 1 ┆ 3 ┆ 2.596 ┆ 0.0 ┆ 0.0 ┆ -10.0 ┆ 0.0 ┆ 0.0 │
186│ 102 ┆ 1 ┆ 2 ┆ 4 ┆ 1.33 ┆ 0.356 ┆ 4.476 ┆ -10.0 ┆ 15.0 ┆ 0.0 │
187│ … ┆ … ┆ … ┆ … ┆ … ┆ … ┆ … ┆ … ┆ … ┆ … │
188│ 423 ┆ 4 ┆ 23 ┆ 91 ┆ 2.248 ┆ -1.298 ┆ 0.0 ┆ -10.0 ┆ -30.0 ┆ 0.0 │
189│ 124 ┆ 1 ┆ 24 ┆ 92 ┆ 1.33 ┆ -0.357 ┆ 4.476 ┆ -10.0 ┆ -15.0 ┆ 0.0 │
190│ 224 ┆ 2 ┆ 24 ┆ 93 ┆ 2.507 ┆ -0.672 ┆ 1.457 ┆ -10.0 ┆ -15.0 ┆ 0.0 │
191│ 324 ┆ 3 ┆ 24 ┆ 94 ┆ 2.507 ┆ -0.672 ┆ 0.728 ┆ -10.0 ┆ -15.0 ┆ 0.0 │
192│ 424 ┆ 4 ┆ 24 ┆ 95 ┆ 2.507 ┆ -0.672 ┆ 0.0 ┆ -10.0 ┆ -15.0 ┆ 0.0 │
193└────────┴──────┴───────────┴────────┴───────┴────────┴───────┴────────────┴──────────┴───────────┘
194 ```
195 """
196
197 # Checking Polars version.
198 version = [int(i) for i in pl.__version__.split(".")]
199 major = version[0]
200 minor = version[1]
201 if major == 1:
202 assert minor > 26 and minor != 31, "Bad polars version"
203 # Need 1.25.2+ for read_csv to enforce pl.Enum checks
204 # see https://github.com/pola-rs/polars/issues/20251
205 # Need NOT 1.31 due to a known bug related to `pl.concat_arr()`,
206 # see https://stackoverflow.com/a/79682731/21955752
207
208 MI_antenna_phi_sector_mapping = (
209 read_global_channel_mapping()
210 .filter(pl.col("AntType") == "MI", pl.col("Pol") == "V")
211 .select("AntNum", "AntIdx", "PhiSector", "Ring")
212 )
213
214 ants: pl.DataFrame = (
215 pl.read_csv(
216 qrh_dot_dat,
217 comment_prefix="#",
218 schema_overrides={
219 "AntNum": ALL_PUEO_ANTENNA_NAMES,
220 "Pitch[deg]": pl.Float64,
221 "Yaw[deg]": pl.Float64,
222 "Roll[deg]": pl.Float64,
223 }
224 )
225 .join(MI_antenna_phi_sector_mapping, on="AntNum")
226 .select(
227 pl.col("AntNum", "Ring", "PhiSector", "AntIdx"),
228 pl.col("X[m]", "Y[m]", "Z[m]", "Pitch[deg]", "Yaw[deg]", "Roll[deg]")
229 )
230
231 )
232 return ants.sort("AntNum")
233
234
235def get_MI_nominal_phase_center(face_centers: pl.DataFrame, coordinates: str = None) -> pl.DataFrame:
236 """!Takes the antenna **face centers** and returns the (nominal) antenna **phase centers**
237 @ingroup AAA
238 @param[in] face_centers The output of #read_MI_antenna_geometry
239 @param[in] coordinates can be "cylindrical" or default (Cartesian)
240 @retval phase_centers Same as `face_centers` except `X[m]`, `Y[m]`, and `Z[m]` now represent
241 phase centers.
242
243* Essential columns: `face_centers` **must** contain `X[m]` and `Y[m]`
244* The nominal phase center is obtained by taking the face center and moving radially inward by 30 cm
245 (cylindrical coordinates).
246* If `coordinates` is not specified then Cartesian coordinates is used, and the output schema is
247```
248$ AntNum <enum>
249$ Ring <u8>
250$ PhiSector <u8>
251$ AntIdx <u32>
252$ X[m] <f64>
253$ Y[m] <f64>
254$ Z[m] <f64>
255$ Pitch[deg] <f64>
256$ Yaw[deg] <f64>
257$ Roll[deg] <f64>
258```
259* If `coordinates` is `cylindrical`, then the ouput schema is
260```
261$ AntNum <enum>
262$ Ring <u8>
263$ PhiSector <u8>
264$ AntIdx <u32>
265$ Z[m] <f64>
266$ Pitch[deg] <f64>
267$ Yaw[deg] <f64>
268$ Roll[deg] <f64>
269$ Rho[m] <f64>
270$ Phi[rad] <f64>
271```
272 """
273
274 x = pl.col("X[m]")
275 y = pl.col("Y[m]")
276
277 rho = (x ** 2 + y ** 2).sqrt() - 0.3
278 phi = pl.arctan2(y, x)
279
280 new_x = rho * phi.cos()
281 new_y = rho * phi.sin()
282
283 if coordinates is None: # Cartesian
284 phase_centers = face_centers.with_columns(new_x.alias("X[m]"), new_y.alias("Y[m]"))
285
286 elif coordinates == "cylindrical":
287 phase_centers = face_centers.select(
288 pl.all().exclude("X[m]", "Y[m]"),
289 rho.alias("Rho[m]"),
290 phi.alias("Phi[rad]")
291 )
292 else:
293 print(f"I can't handle this coordinate system: {coordinates}")
294 exit(1)
295
296 return phase_centers
297
298
299if __name__ == "__main__":
300 pl.Config().set_tbl_cols(10)
301
302 print(read_global_channel_mapping())
303
304 ants = read_MI_antenna_geometry(
305 os.environ.get("PUEO_UTIL_INSTALL_DIR") / Path("share/pueo/geometry/jun25/qrh.dat")
306 )
307 print(ants)
308
309 print(get_MI_nominal_phase_center(ants))