xradio 1.0.1__py3-none-any.whl → 1.1.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.
Files changed (44) hide show
  1. xradio/_utils/_casacore/casacore_from_casatools.py +1 -1
  2. xradio/_utils/dict_helpers.py +38 -7
  3. xradio/_utils/list_and_array.py +26 -3
  4. xradio/_utils/schema.py +44 -0
  5. xradio/_utils/xarray_helpers.py +63 -0
  6. xradio/_utils/zarr/common.py +4 -2
  7. xradio/image/__init__.py +4 -2
  8. xradio/image/_util/_casacore/common.py +2 -1
  9. xradio/image/_util/_casacore/xds_from_casacore.py +105 -51
  10. xradio/image/_util/_casacore/xds_to_casacore.py +117 -52
  11. xradio/image/_util/_fits/xds_from_fits.py +124 -36
  12. xradio/image/_util/_zarr/common.py +0 -1
  13. xradio/image/_util/casacore.py +133 -16
  14. xradio/image/_util/common.py +6 -5
  15. xradio/image/_util/image_factory.py +466 -27
  16. xradio/image/image.py +72 -100
  17. xradio/image/image_xds.py +262 -0
  18. xradio/image/schema.py +85 -0
  19. xradio/measurement_set/__init__.py +5 -4
  20. xradio/measurement_set/_utils/_msv2/_tables/read.py +7 -3
  21. xradio/measurement_set/_utils/_msv2/conversion.py +6 -9
  22. xradio/measurement_set/_utils/_msv2/create_field_and_source_xds.py +1 -0
  23. xradio/measurement_set/_utils/_msv2/msv4_sub_xdss.py +1 -1
  24. xradio/measurement_set/_utils/_utils/interpolate.py +5 -0
  25. xradio/measurement_set/_utils/_utils/partition_attrs.py +0 -1
  26. xradio/measurement_set/convert_msv2_to_processing_set.py +9 -9
  27. xradio/measurement_set/load_processing_set.py +2 -2
  28. xradio/measurement_set/measurement_set_xdt.py +83 -93
  29. xradio/measurement_set/open_processing_set.py +7 -3
  30. xradio/measurement_set/processing_set_xdt.py +33 -26
  31. xradio/schema/check.py +70 -19
  32. xradio/schema/common.py +0 -1
  33. xradio/testing/__init__.py +0 -0
  34. xradio/testing/_utils/__template__.py +58 -0
  35. xradio/testing/measurement_set/__init__.py +58 -0
  36. xradio/testing/measurement_set/checker.py +131 -0
  37. xradio/testing/measurement_set/io.py +22 -0
  38. xradio/testing/measurement_set/msv2_io.py +1854 -0
  39. {xradio-1.0.1.dist-info → xradio-1.1.0.dist-info}/METADATA +64 -23
  40. xradio-1.1.0.dist-info/RECORD +75 -0
  41. {xradio-1.0.1.dist-info → xradio-1.1.0.dist-info}/WHEEL +1 -1
  42. xradio-1.0.1.dist-info/RECORD +0 -66
  43. {xradio-1.0.1.dist-info → xradio-1.1.0.dist-info}/licenses/LICENSE.txt +0 -0
  44. {xradio-1.0.1.dist-info → xradio-1.1.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1854 @@
1
+ """
2
+ Functions to generate MeasurementSets. The motivation is to generate on-the-fly MSs in
3
+ unit tests that can be used in pytest fixtures or external projects that import
4
+ xradio.testing.
5
+
6
+ All functions that depend on casacore have been moved to this module.
7
+ """
8
+
9
+ from contextlib import contextmanager
10
+ import datetime
11
+ import itertools
12
+ from pathlib import Path
13
+ from typing import Generator, Tuple, Dict, Any, Optional, Iterable
14
+
15
+ import numpy as np
16
+
17
+ import casacore.tables as tables
18
+ from casacore.tables import default_ms, default_ms_subtable
19
+ from casacore.tables.tableutil import makedminfo, maketabdesc
20
+ from casacore.tables.msutil import complete_ms_desc, makearrcoldesc, required_ms_desc
21
+
22
+ # 2 observations, 2 fields, 2 states
23
+ # 2 SPWs, 4 polarizations
24
+ default_ms_descr = {
25
+ "nrows_per_ddi": 300,
26
+ "nchans": 16,
27
+ "npols": 2,
28
+ "data_cols": ["DATA"], # ['CORRECTED_DATA'],
29
+ # DATA / CORRECTED, etc.
30
+ # subtables needed to test xds structure
31
+ "SPECTRAL_WINDOW": {"0": 0, "1": 1},
32
+ "POLARIZATION": {"0": 0, "1": 1}, # "2": 2, "3": 3},
33
+ "ANTENNA": {"0": 0, "1": 1, "2": 2, "3": 3, "4": 4},
34
+ # subtables neded to test partitioning
35
+ "FIELD": {"0": 0, "1": 1},
36
+ "EPHEMERIDES": {0: 0},
37
+ "SCAN": {
38
+ "1": {
39
+ "0": {"intent": "CAL_ATMOSPHERE#ON_SOURCE"},
40
+ "1": {"intent": "CAL_ATMOSPHERE#OFF_SOURCE"},
41
+ },
42
+ "2": {
43
+ "0": {"intent": "OBSERVE_TARGET#ON_SOURCE"},
44
+ "1": {"intent": "OBSERVE_TARGET#ON_SOURCE"},
45
+ "2": {"intent": "OBSERVE_TARGET#ON_SOURCE"},
46
+ "3": {"intent": "OBSERVE_TARGET#ON_SOURCE"},
47
+ },
48
+ "3": {
49
+ "0": {"intent": "CALIBRATE_DELAY#ON_SOURCE,CALIBRATE_PHASE#ON_SOURCE"},
50
+ "1": {"intent": "CALIBRATE_DELAY#ON_SOURCE,CALIBRATE_PHASE#ON_SOURCE"},
51
+ "2": {"intent": "CALIBRATE_DELAY#ON_SOURCE,CALIBRATE_PHASE#ON_SOURCE"},
52
+ },
53
+ },
54
+ # CALIBRATE_ATMOSPHERE#OFF_SOURCE,CALIBRATE_ATMOSPHERE#ON_SOURCE,CALIBRATE_WVR#OFF_SOURCE,CALIBRATE_WVR#ON_SOURCE
55
+ "STATE": {
56
+ "0": {"id": 0, "intent": "CAL_ATMOSPHERE#ON_SOURCE"},
57
+ "1": {"id": 1, "intent": "CAL_ATMOSPHERE#OFF_SOURCE"},
58
+ },
59
+ "OBSERVATION": {"0": 0, "1": 1},
60
+ # Mandatory as per MSv2 but not essential to be able to build xdss
61
+ "FEED": {"0": 0},
62
+ "POINTING": {},
63
+ "PROCESSOR": {"0": 0, "1": 1},
64
+ "SOURCE": {"0": 0, "1": 1},
65
+ # Auto-generated without parameters for now:
66
+ # 'FLAG_CMD': {},
67
+ # 'HISTORY': {},
68
+ }
69
+
70
+
71
+ def _gen_test_ms_impl(
72
+ msname: str,
73
+ descr: dict = None,
74
+ opt_tables: bool = True,
75
+ vlbi_tables: bool = True,
76
+ required_only: bool = True,
77
+ misbehave: bool = False,
78
+ ):
79
+ """
80
+ Generates an MS for testing purposes, including main table and
81
+ subtables, either only the required ones or all. Follows the MSv2
82
+ definitions. The aim is to produce MSs with the minimal structure and
83
+ set of rows to exercise xradio functions.
84
+
85
+ - To be able to effectively start testing some functions, these are
86
+ required: MAIN, DATA_DESCRIPTION, SPECTRAL_WINDOW, POLARIZATION, ANTENNA
87
+ - To be able to start testing partitioning: FIELD, STATE
88
+ - Furhter partitioning testing: observation, processor, feed?
89
+
90
+ - The optional tables SYSCAL, WEATHER are generated with very (too)
91
+ simple contents
92
+
93
+ Parameters
94
+ ----------
95
+ msname : str
96
+ name of MS on disk
97
+ descr : dict (Default value = None)
98
+ MS description, including description of scans, fields,
99
+ intents, etc. By default (empty dict) will create an MS with minimal
100
+ structure (one observation, one array, one scan, one field, etc.)
101
+ opt_tables : bool (Default value = True)
102
+ whether to produce optional (sub)tables, such as SOURCE, WEATHER
103
+ vlbi_tables : bool (Default value = True)
104
+ whether to add optional (sub)tables GAIN_CURVE and PHASE_CAL
105
+ required_only : bool (Default value = True)
106
+ whether to use the complete or required columns spec
107
+ misbehave : bool (Default value = False)
108
+ whether to generate a misbehaving MS. For example, usual or more
109
+ corner case conformance issues such as absence of STATE subtable,
110
+ missing FEED subtable, presence of missing SOURCE_IDs in the FIELD
111
+ subtable, no ASDM_EXECBLOCK table, no PROCESSOR subtable, etc.
112
+ Additional, more minor misbehaviors:
113
+ - irregular INTERVAL in main table, empty SPW names.
114
+ - missing metadata for MAIN/INTERVAL, SPECTRAL_WINDOW/CHAN_WIDTH
115
+ - missing posrefsys in EPHEM metadata
116
+
117
+ Returns
118
+ -------
119
+
120
+ """
121
+ if not descr:
122
+ descr = default_ms_descr
123
+
124
+ # Definitely needed: main table + following subtables: DATA_DESCRIPTPION
125
+ # + SPECTRAL_WINDOW + POLARIZATION
126
+ # + ANTENNNA (just for the names)
127
+ # All these are required to create the xdss (they define data and dims)
128
+ outdescr = gen_main_table(msname, descr, required_only, misbehave)
129
+ gen_subt_ddi(msname, descr["SPECTRAL_WINDOW"], descr["POLARIZATION"])
130
+ gen_subt_spw(msname, descr["SPECTRAL_WINDOW"], descr["nchans"], misbehave=misbehave)
131
+ gen_subt_antenna(msname, descr["ANTENNA"])
132
+ gen_subt_pol_setup(msname, descr["npols"], descr["POLARIZATION"])
133
+
134
+ gen_ephem = not misbehave
135
+ # Also needed for partitioning: FIELD, STATE
136
+ gen_subt_field(msname, descr["FIELD"], gen_ephem=gen_ephem, misbehave=misbehave)
137
+ if not misbehave:
138
+ gen_subt_state(msname, descr["STATE"])
139
+
140
+ # Required by MSv2/v3 but not strictly required to load and partition:
141
+
142
+ # BEAM (only MSv3)
143
+
144
+ # FEED
145
+ gen_subt_feed(
146
+ msname, descr["FEED"], descr["ANTENNA"], descr["SPECTRAL_WINDOW"], misbehave
147
+ )
148
+
149
+ # HISTORY
150
+ gen_subt_history(msname, descr["OBSERVATION"])
151
+
152
+ # OBSERVATION
153
+ gen_subt_observation(msname, descr["OBSERVATION"])
154
+
155
+ # POINTING: TODO
156
+ gen_subt_pointing(msname)
157
+
158
+ # PROCESSOR
159
+ if not misbehave:
160
+ gen_subt_processor(msname, descr["PROCESSOR"])
161
+
162
+ if opt_tables:
163
+ # DOPPLER: no examples seen in test MSs
164
+
165
+ # EPHEMERIDES (only defined in Msv3, never seen in practice)
166
+
167
+ # FLAG_CMD (made optional in MSv3, required in MSv2)
168
+ gen_subt_flag_cmd(msname)
169
+
170
+ # FREQ_OFFSET: no examples seen in test MSs
171
+
172
+ # INTERFEROMETER_MODEL (only MSv3)
173
+
174
+ # PHASED_ARRAY (only MSv3)
175
+
176
+ # QUALITY_FREQUENCY_STATISTIC (only MSv3: listend but never defined)
177
+
178
+ # QUALITY_BASELINE_STATISTIC (only MSv3: listend but never defined)
179
+
180
+ # QUALITY_TIME_STATISTIC (only MSv3: listend but never defined)
181
+
182
+ # SOURCE
183
+ gen_subt_source(msname, descr["SOURCE"], misbehave)
184
+
185
+ # SCAN (Only MSv3)
186
+
187
+ # SYSCAL
188
+ gen_subt_syscal(msname, descr["ANTENNA"])
189
+
190
+ # WEATHER
191
+ gen_subt_weather(msname)
192
+
193
+ # ASDM_* subtables. One simple example
194
+ gen_subt_asdm_receiver(msname)
195
+
196
+ if not misbehave:
197
+ # ASDM_EXECBLOCK is used in create_info_dicts when available
198
+ gen_subt_asdm_execblock(msname)
199
+ # ASDM_STATION is used in create_weather
200
+ gen_subt_asdm_station(msname)
201
+
202
+ if vlbi_tables:
203
+ gen_subt_gain_curve(msname, descr["ANTENNA"])
204
+ gen_subt_phase_cal(msname, descr["ANTENNA"])
205
+
206
+ outdescr["params"] = {
207
+ "opt_tables": opt_tables,
208
+ "vlbi_tables": vlbi_tables,
209
+ "misbehave": misbehave,
210
+ }
211
+
212
+ return outdescr
213
+
214
+
215
+ def gen_test_ms(
216
+ msname: str,
217
+ *,
218
+ descr: Optional[Dict[str, Any]] = None,
219
+ opt_tables: bool = True,
220
+ vlbi_tables: bool = True,
221
+ required_only: bool = True,
222
+ misbehave: bool = False,
223
+ ) -> Tuple[str, Dict[str, Any]]:
224
+ """
225
+ Generate an MS on disk and return both the path and the generated description.
226
+
227
+ This is the high-level helper used by tests and benchmarks. The lower-level
228
+ implementation is `_gen_test_ms_impl`.
229
+ """
230
+
231
+ ms_descr = _gen_test_ms_impl(
232
+ msname,
233
+ descr,
234
+ opt_tables=opt_tables,
235
+ vlbi_tables=vlbi_tables,
236
+ required_only=required_only,
237
+ misbehave=misbehave,
238
+ )
239
+ return msname, ms_descr
240
+
241
+
242
+ def gen_minimal_ms(
243
+ msname: str = "test_msv2_minimal_required.ms",
244
+ *,
245
+ misbehave: bool = False,
246
+ include_optional_tables: bool = True,
247
+ include_vlbi_tables: bool = True,
248
+ ) -> Tuple[str, Dict[str, Any]]:
249
+ """
250
+ Generate a minimal MSv2 dataset with sensible defaults used across tests/benchmarks.
251
+ """
252
+
253
+ return gen_test_ms(
254
+ msname,
255
+ opt_tables=include_optional_tables,
256
+ vlbi_tables=include_vlbi_tables,
257
+ required_only=True,
258
+ misbehave=misbehave,
259
+ )
260
+
261
+
262
+ def make_ms_empty(name: str, descr: dict = None, complete: bool = False):
263
+ """
264
+ OLD simple function. makes empty (0 rows) MSs
265
+
266
+ Parameters
267
+ ----------
268
+ name : str
269
+
270
+ descr : dict (Default value = None)
271
+
272
+ complete : bool (Default value = False)
273
+
274
+ Returns
275
+ -------
276
+
277
+ """
278
+ if complete:
279
+ tabdesc = complete_ms_desc("MAIN")
280
+ else:
281
+ tabdesc = required_ms_desc("MAIN")
282
+
283
+ datacoldesc = tables.makearrcoldesc(
284
+ "DATA",
285
+ 0.1 + 0.1j,
286
+ valuetype="complex",
287
+ ndim=2,
288
+ datamanagertype="TiledShapeStMan",
289
+ datamanagergroup="TiledData",
290
+ comment="The data column",
291
+ )
292
+ del datacoldesc["desc"]["shape"]
293
+ tabdesc.update(tables.maketabdesc(datacoldesc))
294
+
295
+ weightspeccoldesc = tables.makearrcoldesc(
296
+ "WEIGHT_SPECTRUM",
297
+ 1.0,
298
+ valuetype="float",
299
+ ndim=2,
300
+ datamanagertype="TiledShapeStMan",
301
+ datamanagergroup="TiledWgtSpectrum",
302
+ comment="Weight for each data point",
303
+ )
304
+ del weightspeccoldesc["desc"]["shape"]
305
+ tabdesc.update(tables.maketabdesc(weightspeccoldesc))
306
+
307
+ vis = tables.default_ms(name, tabdesc=tabdesc, dminfo=makedminfo(tabdesc))
308
+ assert vis.nrows() == 0
309
+
310
+
311
+ def gen_main_table(
312
+ mspath: str, descr: dict, required_only: bool = True, misbehave: bool = False
313
+ ):
314
+ """
315
+ Create main MSv2 table.
316
+ Relies on the required/complete_ms_desc descriptions of columns
317
+
318
+ Simplifications/assumptions:
319
+ - All polarization setups have the same number of correlations/
320
+ - All scans have the same SPWs (all SPWs)
321
+ - Multiple observation: just repeat the same structure (same scans,
322
+ fields, etc.)
323
+ - Can generate multiple *_DATA columns but they all have the same data
324
+ pattern
325
+
326
+ :return: outdescr
327
+
328
+ Parameters
329
+ ----------
330
+ mspath: str :
331
+
332
+ descr: dict :
333
+
334
+ required_only: bool :
335
+ (Default value = True)
336
+
337
+ misbehave : bool (Default value = False)
338
+ all STATE_ID values are set to -1 (assuming a "misbehaved" MS with empty STATE subtable)
339
+
340
+ Returns
341
+ -------
342
+
343
+ """
344
+
345
+ outdescr = descr.copy()
346
+
347
+ if descr == {}:
348
+ nchans = 16
349
+ npols = 2
350
+ else:
351
+ nchans = descr["nchans"]
352
+ npols = descr["npols"]
353
+
354
+ if required_only:
355
+ ms_desc = required_ms_desc("MAIN")
356
+ else:
357
+ ms_desc = complete_ms_desc("MAIN")
358
+
359
+ ms_desc["UVW"].update(
360
+ options=0,
361
+ shape=[3],
362
+ ndim=1,
363
+ dataManagerGroup="UVWGroup",
364
+ dataManagerType="TiledColumnStMan",
365
+ )
366
+ dmgroups_spec = {"UVW": {"DEFAULTTILESHAPE": [3, nchans]}}
367
+
368
+ # data columns. TODO: move to function - make_data_cols_descr
369
+ for data_col_name in descr["data_cols"]:
370
+ data_col_desc = makearrcoldesc(
371
+ data_col_name,
372
+ 0.0,
373
+ options=4,
374
+ valuetype="complex",
375
+ # Beware of the additional column description entry - not present in casatestdata
376
+ shape=[nchans, npols],
377
+ ndim=2,
378
+ datamanagertype="TiledColumnStMan",
379
+ datamanagergroup="DataGroup",
380
+ comment="added by gen_test_ms",
381
+ )
382
+ ms_desc.update(maketabdesc(data_col_desc))
383
+
384
+ dmgroups_spec.update(
385
+ {"DataColsGroup": {"DEFAULTTILESHAPE": [nchans, npols, nchans]}}
386
+ )
387
+ ms_data_man_info = makedminfo(ms_desc, dmgroups_spec)
388
+
389
+ if "STATE" not in descr:
390
+ vis = tables.default_ms(mspath, tabdesc=ms_desc, dminfo=makedminfo(ms_desc))
391
+ assert vis.nrows() == 0
392
+ return
393
+ # else:
394
+ # populate (TODO)
395
+
396
+ # Key columns (that define data rows):
397
+ # - TIME: given in descr
398
+ # - ANTENNA1, ANTENNA2: given in descr
399
+ # - DATA_DESC_ID, PROCESSOR_ID: given in descr
400
+ # - FEED1, FEED2, assumed 0
401
+ # - Not considered: ANTENNA3, FEED3, PHASE_ID, TIME_EXTRA_PREC
402
+
403
+ with default_ms(mspath, ms_desc, ms_data_man_info) as msv2:
404
+ desc = msv2.getcoldesc("UVW")
405
+ assert desc["dataManagerType"] == "TiledColumnStMan"
406
+ dminfo = msv2.getdminfo("UVW")
407
+ assert dminfo["NAME"] == "UVWGroup"
408
+
409
+ # Figure out amount of rows and related IDs
410
+ nrows = descr["nrows_per_ddi"]
411
+ dd_pairs = list(
412
+ itertools.product(
413
+ list(descr["SPECTRAL_WINDOW"].values()), descr["POLARIZATION"].values()
414
+ )
415
+ )
416
+ nddis = len(dd_pairs)
417
+ outdescr["nddis"] = nddis
418
+ dd_ids = np.arange(nddis)
419
+ nrows *= nddis
420
+ # problem: TIME - SPW
421
+
422
+ msv2.addrows(nrows)
423
+ # trasnposing so that indices increase with channel number, not pol
424
+ vis_val = np.arange(nchans * npols).reshape(
425
+ (npols, nchans)
426
+ ).transpose() * np.complex64(1 - 1j)
427
+ # vis_val *= np.arange(
428
+ # msv2.putcell("CORRECTED_DATA", 0, vis_val)
429
+ for data_col in descr["data_cols"]:
430
+ msv2.putcol(data_col, np.broadcast_to(vis_val, (nrows, nchans, npols)))
431
+
432
+ # === Key attributes ===
433
+ # Make Ids
434
+
435
+ # TIME
436
+ CASACORE_TO_DATETIME_CORRECTION = 3_506_716_800.0
437
+ start = (
438
+ datetime.datetime(
439
+ 2025, 5, 1, 1, 1, tzinfo=datetime.timezone.utc
440
+ ).timestamp()
441
+ + CASACORE_TO_DATETIME_CORRECTION
442
+ )
443
+ time_col = np.arange(nrows) + start
444
+ msv2.putcol("TIME", time_col)
445
+
446
+ # (TIME_EXTRA_PREC): nothing for now
447
+
448
+ nants = len(descr["ANTENNA"])
449
+ # ANTENNA1, ANTENNA2
450
+ # baselines = list(itertools.product(ants, ants))
451
+ # combinations without repetitions: baselines as list of tuples
452
+ ants = np.arange(nants)
453
+ baselines = list(itertools.combinations(ants, 2))
454
+ ant1_ant2 = list(zip(*baselines))
455
+
456
+ # TODO: needs fixes for rounding
457
+ _reps = np.tile(ant1_ant2[0], int(nrows / len(baselines)))
458
+ msv2.putcol("ANTENNA1", np.tile(ant1_ant2[0], int(nrows / len(baselines))))
459
+ msv2.putcol("ANTENNA2", np.tile(ant1_ant2[1], int(nrows / len(baselines))))
460
+
461
+ # (ANTENNA3): nothing for now
462
+
463
+ msv2.putcol("FEED1", np.broadcast_to(0, (nrows)))
464
+
465
+ msv2.putcol("FEED2", np.broadcast_to(0, (nrows)))
466
+
467
+ # (FEED3): nothing for now
468
+
469
+ # DATA_DESC_ID
470
+ # msv2.putcol("DATA_DESC_ID", np.broadcast_to(0, (nrows)))
471
+ ddi_col = np.repeat(dd_ids, nrows / nddis)
472
+ msv2.putcol("DATA_DESC_ID", np.broadcast_to(ddi_col, (nrows)))
473
+
474
+ msv2.putcol("PROCESSOR_ID", np.broadcast_to(0, (nrows)))
475
+
476
+ msv2.putcol("FIELD_ID", np.broadcast_to(0, (nrows)))
477
+
478
+ # PHASE_ID: nothing for now
479
+
480
+ # === Non-key attributes ===
481
+
482
+ # The ones that are IDs
483
+ msv2.putcol("OBSERVATION_ID", np.broadcast_to(0, (nrows)))
484
+
485
+ msv2.putcol("ARRAY_ID", np.broadcast_to(0, (nrows)))
486
+
487
+ msv2.putcol("SCAN_NUMBER", np.broadcast_to(1, (nrows)))
488
+
489
+ # STATE_ID: if no states/intents => all STATE_ID = -1
490
+ if misbehave:
491
+ msv2.putcol("STATE_ID", np.broadcast_to(-1, (nrows)))
492
+ else:
493
+ msv2.putcol("STATE_ID", np.broadcast_to(0, (nrows)))
494
+
495
+ # Make other scalar / inc columns
496
+
497
+ interval = 1.0
498
+ msv2.putcol("INTERVAL", np.broadcast_to(interval, (nrows)))
499
+ if misbehave:
500
+ msv2.putcell("INTERVAL", 0, interval - 0.1)
501
+ msv2.removecolkeyword("INTERVAL", "QuantumUnits")
502
+
503
+ exposure = 0.9 * interval
504
+ msv2.putcol("EXPOSURE", np.broadcast_to(exposure, (nrows)))
505
+
506
+ # TIME_CENTROID
507
+ msv2.putcol("TIME_CENTROID", time_col)
508
+
509
+ # (PULSAR_BIN)
510
+
511
+ # (PULSAR_GATE_ID)
512
+
513
+ # (BASELINE_REF)
514
+
515
+ # UVW
516
+ msv2.putcol("UVW", np.broadcast_to([1.0, 0.5, 1.5], (nrows, 3)))
517
+
518
+ # (UVW2)
519
+
520
+ # (DATA)
521
+
522
+ # (FLOAT_DATA)
523
+
524
+ # (VIDEO_POINT)
525
+
526
+ # (LAG_DATA)
527
+
528
+ # SIGMA
529
+ msv2.putcol("SIGMA", np.broadcast_to(1.0, (nrows, npols)))
530
+
531
+ # (SIGMA_SPECTRUM)
532
+
533
+ # WEIGHT
534
+ # msv2.putcol("WEIGHT", np.broadcast_to(1.0, (nrows, npols)))
535
+
536
+ # (WEIGHT_SPECTRUM)
537
+
538
+ msv2.putcol("FLAG", np.broadcast_to(False, (nrows, nchans, npols)))
539
+
540
+ # FLAG_CATEGORY: leave empty
541
+
542
+ msv2.putcol("FLAG_ROW", np.broadcast_to(False, (nrows)))
543
+
544
+ return outdescr
545
+
546
+
547
+ def gen_subt_ddi(mspath: str, spws_descr: dict, pol_setup_descr: dict):
548
+ """
549
+ Populates DATA_DESCRIPTION
550
+ Q: spws_descr with IDs in keys? IDs in MSv2 are implicit 0...n-1
551
+ """
552
+ import itertools
553
+
554
+ # TODO generate spw-pol
555
+ with tables.table(
556
+ mspath + "::DATA_DESCRIPTION", ack=False, readonly=False
557
+ ) as ddi_tbl:
558
+ nrows = len(spws_descr) * len(pol_setup_descr)
559
+ ddi_tbl.addrows(nrows)
560
+
561
+ ddis = list(
562
+ itertools.product(list(spws_descr.values()), pol_setup_descr.values())
563
+ )
564
+ spw_ids = list(zip(*ddis))[0]
565
+ pol_ids = list(zip(*ddis))[1]
566
+ ddi_tbl.putcol("SPECTRAL_WINDOW_ID", spw_ids)
567
+ ddi_tbl.putcol("POLARIZATION_ID", pol_ids)
568
+ ddi_tbl.putcol("FLAG_ROW", np.broadcast_to(False, nrows))
569
+
570
+
571
+ def gen_subt_spw(mspath: str, spw_descr: dict, nchans: int, misbehave=False):
572
+ """
573
+ Populates SPECTRAL_WINDOW
574
+ """
575
+
576
+ nspws = len(spw_descr)
577
+ with tables.table(
578
+ mspath + "::SPECTRAL_WINDOW", ack=False, readonly=False
579
+ ) as spw_tbl:
580
+ spw_tbl.addrows(nspws)
581
+ # Not in MSv2
582
+ # spw_tbl.putcol("SPECTRAL_WINDOW_ID", list(spw_descr.keys()))
583
+ if misbehave:
584
+ names = ["" for idx in range(nspws)]
585
+ else:
586
+ names = [f"unspecified_test#{idx}" for idx in range(nspws)]
587
+ spw_tbl.putcol("NAME", names)
588
+ # spw_tbl.putcol("REF_FREQUENCY", nspws*[0.9e9])
589
+ # spw_tbl.putcol("TOTAL_BANDWIDTH", nspws*[0.020])
590
+
591
+ for spw in range(nspws):
592
+ # nchans = spw_descr[spw]['NUM_CHAN']
593
+ # spw_tbl.addrows(1) # 1
594
+
595
+ # spw_tbl.putcell("NAME", spw, "unspecified_test")
596
+ spw_tbl.putcell("REF_FREQUENCY", spw, 0.9e9)
597
+
598
+ spw_tbl.putcol("NUM_CHAN", nchans, startrow=spw, nrow=1)
599
+ widths = np.full(nchans, 20000.0)
600
+ spw_tbl.putcell("CHAN_WIDTH", spw, widths)
601
+ if misbehave:
602
+ kws = spw_tbl.getcolkeywords("CHAN_WIDTH")
603
+ units_kw = "QuantumUnits"
604
+ if units_kw in kws:
605
+ spw_tbl.removecolkeyword("CHAN_WIDTH", "QuantumUnits")
606
+ # or alternatively
607
+ # spw_tbl.putcol("CHAN_WIDTH", np.full((1, nchans), 20000.0), startrow=spw, nrow=1)
608
+ spw_tbl.putcell("CHAN_FREQ", spw, np.full(nchans, 10e0))
609
+ spw_tbl.putcell("EFFECTIVE_BW", spw, widths)
610
+ spw_tbl.putcell("TOTAL_BANDWIDTH", spw, sum(widths))
611
+ spw_tbl.putcell("RESOLUTION", spw, widths)
612
+
613
+
614
+ def gen_subt_pol_setup(mspath: str, npols, pol_setup_descr: dict):
615
+ """
616
+ populates POLARIZATION
617
+ """
618
+
619
+ nsetups = len(pol_setup_descr)
620
+
621
+ with tables.table(mspath + "::POLARIZATION", ack=False, readonly=False) as pol_tbl:
622
+ pol_tbl.addrows(npols)
623
+ # pol_tbl.putcol("POLARIZATION_ID", list(spw_descr.keys()))
624
+ pol_tbl.putcol("NUM_CORR", nsetups * [npols])
625
+ pol_tbl.putcol("FLAG_ROW", nsetups * [False])
626
+ corr_types = [9, 11, 13, 15]
627
+ pol_tbl.putcol(
628
+ "CORR_TYPE", np.broadcast_to(corr_types[:npols], (nsetups, npols))
629
+ )
630
+ corr_products = [0, 0, 1, 1]
631
+ pol_tbl.putcol(
632
+ "CORR_PRODUCT",
633
+ np.broadcast_to(corr_products[:npols], (nsetups, nsetups, npols)),
634
+ )
635
+
636
+
637
+ def gen_subt_antenna(mspath: str, ant_descr: dict):
638
+ """
639
+ create ANTENNA
640
+ """
641
+
642
+ with tables.table(mspath + "::ANTENNA", ack=False, readonly=False) as ant_tbl:
643
+ nants = len(ant_descr)
644
+ ant_tbl.addrows(nants)
645
+ ant_tbl.putcol("NAME", [f"test_antenna_{idx}" for idx in range(nants)])
646
+ ant_tbl.putcol("STATION", [f"test_station_{idx}" for idx in range(nants)])
647
+ ant_tbl.putcol("DISH_DIAMETER", nants * [12])
648
+ ant_tbl.putcol("FLAG_ROW", nants * [False])
649
+ ant_tbl.putcol("POSITION", np.broadcast_to([0.01, 0.02, 0.03], (nants, 3)))
650
+
651
+
652
+ # field and state very relevant for partitions/sub-MSv2
653
+ def gen_subt_field(
654
+ mspath: str, fields_descr: dict, gen_ephem: bool = True, misbehave: bool = False
655
+ ):
656
+ """
657
+ creates FIELD
658
+
659
+ only supports polynomials with 1 coefficient
660
+
661
+ Parameters
662
+ ----------
663
+ mspath : str
664
+ path of output MS
665
+ fields_descr : dict
666
+ fields description
667
+ gen_ephem : bool
668
+ whether to generate ephemeris fields (with EPHEM pseudo-sub tables
669
+ misbehave : bool
670
+ if True, some missing SOURCE_IDs will be added
671
+
672
+ Returns
673
+ -------
674
+
675
+ """
676
+
677
+ with tables.table(mspath + "::FIELD", ack=False, readonly=False) as fld_tbl:
678
+ nfields = len(fields_descr)
679
+ fld_tbl.addrows(nfields)
680
+ fld_tbl.putcol("NAME", nfields * ["NGC3031"])
681
+
682
+ npoly = 0
683
+ fld_tbl.putcol("NUM_POLY", nfields * [npoly])
684
+
685
+ adir = np.deg2rad([30, 45])
686
+ for dir_col in ["DELAY_DIR", "PHASE_DIR", "REFERENCE_DIR"]:
687
+ fld_tbl.putcol(dir_col, np.broadcast_to(adir, (nfields, npoly + 1, 2)))
688
+
689
+ fld_tbl.putcol("SOURCE_ID", np.arange(nfields))
690
+ if misbehave:
691
+ fld_tbl.putcell("SOURCE_ID", nfields - 1, nfields + 3)
692
+
693
+ if gen_ephem:
694
+ tabdesc_ephemeris_id = {
695
+ "EPHEMERIS_ID": {
696
+ "valueType": "int",
697
+ "dataManagerType": "StandardStMan",
698
+ "dataManagerGroup": "StandardStMan",
699
+ "option": 0,
700
+ "maxlen": 0,
701
+ "ndim": 0,
702
+ "comment": "comment...",
703
+ }
704
+ }
705
+ fld_tbl.addcols(tabdesc_ephemeris_id)
706
+ fld_tbl.putcol("EPHEMERIS_ID", np.broadcast_to(0, (nfields, 1)))
707
+
708
+ if gen_ephem:
709
+ gen_subt_ephem(mspath, misbehave)
710
+
711
+
712
+ def gen_subt_ephem(mspath: str, misbehave=False):
713
+ """
714
+ Creates a phony ephemerides table under the FIELD subtable.
715
+ The usual tables... context or 'default_ms_subtable' not working. Needs
716
+ manual coldesc /not in MSv2/3
717
+
718
+ Parameters
719
+ ----------
720
+ mspath : str
721
+ path of output MS
722
+ misbehave : bool
723
+ if True, some metadata (keywords) will be missing: posrefsys, and metadata
724
+ of column 'RA'
725
+
726
+ Returns
727
+ -------
728
+
729
+ """
730
+ # with open_opt_subtable(mspath, "FIELD/EPHEM0_FIELD.tab") as wtbl:
731
+ ephem0_path = Path(mspath) / "FIELD" / "EPHEM0_FIELDNAME.tab"
732
+ tabdesc = {
733
+ "MJD": {
734
+ "valueType": "double",
735
+ "dataManagerType": "StandardStMan",
736
+ "dataManagerGroup": "StandardStMan",
737
+ "option": 0,
738
+ "maxlen": 0,
739
+ "comment": "comment...",
740
+ "keywords": {
741
+ "QuantumUnits": ["s"],
742
+ "MEASINFO": {"type": "epoch", "Ref": "bogus MJD"},
743
+ },
744
+ },
745
+ "RA": {
746
+ "valueType": "double",
747
+ "dataManagerType": "StandardStMan",
748
+ "dataManagerGroup": "StandardStMan",
749
+ "option": 0,
750
+ "maxlen": 0,
751
+ "comment": "comment...",
752
+ "keywords": {
753
+ "UNIT": "deg",
754
+ },
755
+ },
756
+ "DEC": {
757
+ "valueType": "double",
758
+ "dataManagerType": "StandardStMan",
759
+ "dataManagerGroup": "StandardStMan",
760
+ "option": 0,
761
+ "maxlen": 0,
762
+ "comment": "comment...",
763
+ "keywords": {
764
+ "UNIT": "deg",
765
+ },
766
+ },
767
+ "Rho": {
768
+ "valueType": "double",
769
+ "dataManagerType": "StandardStMan",
770
+ "dataManagerGroup": "StandardStMan",
771
+ "option": 0,
772
+ "maxlen": 0,
773
+ "comment": "comment...",
774
+ "keywords": {
775
+ "UNIT": "AU",
776
+ },
777
+ },
778
+ "RadVel": {
779
+ "valueType": "double",
780
+ "dataManagerType": "StandardStMan",
781
+ "dataManagerGroup": "StandardStMan",
782
+ "option": 0,
783
+ "maxlen": 0,
784
+ "comment": "comment...",
785
+ "keywords": {
786
+ "UNIT": "AU/d",
787
+ },
788
+ },
789
+ "DiskLong": {
790
+ "valueType": "double",
791
+ "dataManagerType": "StandardStMan",
792
+ "dataManagerGroup": "StandardStMan",
793
+ "option": 0,
794
+ "maxlen": 0,
795
+ "comment": "comment...",
796
+ "keywords": {
797
+ "UNIT": "deg",
798
+ },
799
+ },
800
+ "DiskLat": {
801
+ "valueType": "double",
802
+ "dataManagerType": "StandardStMan",
803
+ "dataManagerGroup": "StandardStMan",
804
+ "option": 0,
805
+ "maxlen": 0,
806
+ "comment": "comment...",
807
+ "keywords": {
808
+ "UNIT": "deg",
809
+ },
810
+ },
811
+ "SI_lon": {
812
+ "valueType": "double",
813
+ "dataManagerType": "StandardStMan",
814
+ "dataManagerGroup": "StandardStMan",
815
+ "option": 0,
816
+ "maxlen": 0,
817
+ "comment": "comment...",
818
+ "keywords": {
819
+ "UNIT": "deg",
820
+ },
821
+ },
822
+ "SI_lat": {
823
+ "valueType": "double",
824
+ "dataManagerType": "StandardStMan",
825
+ "dataManagerGroup": "StandardStMan",
826
+ "option": 0,
827
+ "maxlen": 0,
828
+ "comment": "comment...",
829
+ "keywords": {
830
+ "UNIT": "deg",
831
+ },
832
+ },
833
+ "r": {
834
+ "valueType": "double",
835
+ "dataManagerType": "StandardStMan",
836
+ "dataManagerGroup": "StandardStMan",
837
+ "option": 0,
838
+ "maxlen": 0,
839
+ "comment": "comment...",
840
+ "keywords": {
841
+ "UNIT": "AU",
842
+ },
843
+ },
844
+ "rdot": {
845
+ "valueType": "double",
846
+ "dataManagerType": "StandardStMan",
847
+ "dataManagerGroup": "StandardStMan",
848
+ "option": 0,
849
+ "maxlen": 0,
850
+ "comment": "comment...",
851
+ "keywords": {
852
+ "UNIT": "km/s",
853
+ },
854
+ },
855
+ "phang": {
856
+ "valueType": "double",
857
+ "dataManagerType": "StandardStMan",
858
+ "dataManagerGroup": "StandardStMan",
859
+ "option": 0,
860
+ "maxlen": 0,
861
+ "comment": "comment...",
862
+ "keywords": {
863
+ "UNIT": "deg",
864
+ },
865
+ },
866
+ }
867
+
868
+ keywords = {
869
+ "VS_CREATE": "2021/01/02/12:33",
870
+ "VS_DATE": "2021/01/02/12:33",
871
+ "VS_VERSION": "0001.0001",
872
+ "MJD0": 58941.4,
873
+ "dMJD": 0.0138889,
874
+ "NAME": "Test_ephem_object",
875
+ "obsloc": "GEOCENTRIC",
876
+ "GeoLong": 0.0,
877
+ "GeoLat": 0.0,
878
+ "GeoDist": 0.0,
879
+ "posrefsys": "ICRF/ICRS",
880
+ }
881
+ with tables.table(
882
+ str(ephem0_path), tabledesc=tabdesc, nrow=1, readonly=False, ack=False
883
+ ) as tbl:
884
+
885
+ if misbehave:
886
+ del tabdesc["RA"]["keywords"]
887
+ del keywords["posrefsys"]
888
+
889
+ tbl.putcol("MJD", 50000)
890
+ tbl.putcol("RA", 230.334)
891
+ tbl.putcol("DEC", -15.678)
892
+ tbl.putcol("Rho", 0.55)
893
+ tbl.putcol("RadVel", 0.004)
894
+ tbl.putcol("DiskLong", 333.01)
895
+ tbl.putcol("DiskLat", 4.09)
896
+ tbl.putcol("SI_lon", 338.81)
897
+ tbl.putcol("SI_lat", 2.44)
898
+ tbl.putcol("r", 9.1234)
899
+ tbl.putcol("rdot", -1.234)
900
+ tbl.putcol("phang", 5.6789)
901
+ tbl.putkeywords(keywords)
902
+
903
+
904
+ def gen_subt_state(mspath: str, states_descr: dict):
905
+ """
906
+ populates STATE
907
+ """
908
+
909
+ # intents in OBS_MODE strings
910
+ with tables.table(mspath + "::STATE", ack=False, readonly=False) as st_tbl:
911
+ nstates = len(states_descr)
912
+ st_tbl.addrows(nstates)
913
+ st_tbl.putcol("SIG", nstates * [True])
914
+ st_tbl.putcol("REF", nstates * [False])
915
+ st_tbl.putcol("CAL", nstates * [0.0])
916
+ st_tbl.putcol("LOAD", nstates * [0.0])
917
+ # TODO: generate subscan ids for potentially multiple scans
918
+ st_tbl.putcol("SUB_SCAN", 1 + np.arange(nstates))
919
+ st_tbl.putcol("OBS_MODE", nstates * ["scan_intent#subscan_intent"])
920
+ st_tbl.putcol("FLAG_ROW", nstates * [False])
921
+
922
+
923
+ # Other - optional subtables
924
+
925
+
926
+ @contextmanager
927
+ def open_opt_subtable(
928
+ mspath: str, tbl_name: str
929
+ ) -> Generator[tables.table, None, None]:
930
+ """
931
+ Opens an (optional) subtable of an MS. This can open tables not included
932
+ in the default_ms definition
933
+
934
+ Parameters
935
+ ----------
936
+ mspath : str
937
+ path of output MS
938
+ tbl_name : str
939
+ name of the subtable (WEATHER, etc.). Requires a known optional MS
940
+ subtable name that has a known table description.
941
+
942
+ Returns
943
+ -------
944
+ Generator[tables.table, None, None]
945
+ context for an optional subtable created as per MSv2 specs
946
+ """
947
+ subt_desc = tables.complete_ms_desc(tbl_name)
948
+ # table = tables.table(mspath + "/" + tbl_name, tabledesc=subt_desc,
949
+ # dminfo=makedminfo(subt_desc), ack=False, readonly=False)
950
+ table = default_ms_subtable(tbl_name, mspath + "/" + tbl_name, subt_desc)
951
+ try:
952
+ yield table
953
+ finally:
954
+ table.close()
955
+
956
+
957
+ def gen_subt_source(mspath: str, src_descr: dict, misbehave: bool = False):
958
+ """
959
+ Populate SOURCE subtable, with time dependent source info
960
+
961
+ Parameters
962
+ ----------
963
+ mspath : str
964
+ path of output MS
965
+ src_descr : dict
966
+ sources description
967
+ misbehave : bool
968
+ if misbehave, the NUM_LINES and related columns will be missing
969
+
970
+ Returns
971
+ -------
972
+
973
+ """
974
+
975
+ # SOURCE is not included in default_ms
976
+ # with tables.table(mspath + "::SOURCE", ack=False, readonly=False) as src_tbl:
977
+ with open_opt_subtable(mspath, "SOURCE") as src_tbl:
978
+ nsrcs = len(src_descr)
979
+ src_tbl.addrows(nsrcs)
980
+ # if misbehave:
981
+ # src_tbl.putcol("SOURCE_ID", np.repeat(-1, len(src_descr)))
982
+ src_tbl.putcol("SOURCE_ID", list(src_descr.values()))
983
+ src_tbl.putcol("TIME", np.broadcast_to(0, (nsrcs)))
984
+ src_tbl.putcol("INTERVAL", np.broadcast_to(0, (nsrcs)))
985
+ src_tbl.putcol("SPECTRAL_WINDOW_ID", np.broadcast_to(0, (nsrcs)))
986
+
987
+ if not misbehave:
988
+ nlines = 3
989
+ src_tbl.putcol("NUM_LINES", np.repeat(nlines, nsrcs))
990
+
991
+ src_tbl.putcol("NAME", np.broadcast_to("test_source_name", (nsrcs)))
992
+ src_tbl.putcol("CALIBRATION_GROUP", np.broadcast_to(0, (nsrcs)))
993
+ src_tbl.putcol("CODE", np.broadcast_to("test_source", (nsrcs)))
994
+ adir = np.deg2rad([29, 34])
995
+ src_tbl.putcol("DIRECTION", np.broadcast_to(adir, (nsrcs, 2)))
996
+ src_tbl.putcol("PROPER_MOTION", np.broadcast_to(adir / 100.0, (nsrcs, 2)))
997
+ # optional cols:
998
+ if not misbehave:
999
+ src_tbl.putcol(
1000
+ "TRANSITION",
1001
+ np.broadcast_to(
1002
+ ["test_line0", "test_line1", "test_line2"], (nsrcs, nlines)
1003
+ ),
1004
+ )
1005
+ src_tbl.putcol(
1006
+ "REST_FREQUENCY", np.broadcast_to([1e9, 2e9, 3e9], (nsrcs, nlines))
1007
+ )
1008
+ src_tbl.putcol("SYSVEL", np.broadcast_to([1e3, 2e3, 3e3], (nsrcs, nlines)))
1009
+ # SOURCE_MODEL is optional and its type is TableRecord!
1010
+ src_tbl.removecols(["SOURCE_MODEL"])
1011
+
1012
+ with tables.table(mspath, ack=False, readonly=False) as main:
1013
+ main.putkeyword("SOURCE", f"Table: {mspath}/SOURCE")
1014
+
1015
+
1016
+ def gen_subt_pointing(mspath: str):
1017
+ """
1018
+ Populate POINTING subtable, with antenna-based pointing info
1019
+
1020
+ Very rudimentary.
1021
+ """
1022
+ # with open_opt_subtable(mspath, "POINTING") as tbl:
1023
+ nrows = 1
1024
+ with tables.table(mspath + "::POINTING", ack=False, readonly=False) as tbl:
1025
+ tbl.addrows(nrows)
1026
+ tbl.putcol("ANTENNA_ID", 0)
1027
+ tbl.putcol("TIME", 2e9)
1028
+ tbl.putcol("INTERVAL", 1e12)
1029
+ tbl.putcol("NAME", "test_pointing_name")
1030
+ tbl.putcol("NUM_POLY", 0)
1031
+ tbl.putcol("TIME_ORIGIN", 1e9)
1032
+ adir = np.deg2rad([28.98, 34.03])
1033
+ tbl.putcol("DIRECTION", np.broadcast_to(adir, (nrows, 1, 2)))
1034
+ tbl.putcol("TARGET", np.broadcast_to(adir + 0.01, (nrows, 1, 2)))
1035
+ tbl.putcol("TRACKING", True)
1036
+
1037
+
1038
+ # Other, more secondary subtables
1039
+
1040
+
1041
+ def gen_subt_feed(
1042
+ mspath: str,
1043
+ feed_descr: dict,
1044
+ ant_descr: str,
1045
+ spw_descr: str,
1046
+ misbehave: bool = False,
1047
+ ):
1048
+ """
1049
+ Populate FEED subtable, with antenna-based pointing info.
1050
+
1051
+ The table is filled for a single FEED_ID (0), for all the available
1052
+ antenna IDs and spectral window IDs. The columns are filled with
1053
+ roughtly similar values as seen in example ALMA MSs imported with
1054
+ importasdm. BEAM_ID is left as -1 (optional BEAM subtable of MSv2 never
1055
+ defined).
1056
+
1057
+ Parameters
1058
+ ----------
1059
+ mspath : str
1060
+ path of output MS
1061
+ feed_descr : dict
1062
+ feed description, only one feed supported
1063
+ ant_descr : str
1064
+
1065
+ spw_descr : str
1066
+
1067
+ misbehave : bool
1068
+ if misbehave, the FEED table will be empty.
1069
+
1070
+ Returns
1071
+ -------
1072
+
1073
+ """
1074
+ if misbehave:
1075
+ return
1076
+
1077
+ with tables.table(mspath + "::FEED", ack=False, readonly=False) as tbl:
1078
+ nfeeds = len(feed_descr)
1079
+ nspws = len(spw_descr)
1080
+ nants = len(ant_descr)
1081
+ nrows = nants * nspws * nfeeds
1082
+ tbl.addrows(nrows)
1083
+ tbl.putcol("ANTENNA_ID", np.tile(np.arange(nants), int(nrows / nants)))
1084
+ tbl.putcol("FEED_ID", np.repeat(0, (nrows)))
1085
+ tbl.putcol(
1086
+ "SPECTRAL_WINDOW_ID", np.repeat(list(spw_descr.values()), (nrows / nspws))
1087
+ )
1088
+ tbl.putcol("TIME", np.broadcast_to(0, (nrows)))
1089
+ tbl.putcol("INTERVAL", np.broadcast_to(5e9, (nrows)))
1090
+ nrecep = 2
1091
+ tbl.putcol("NUM_RECEPTORS", np.broadcast_to(nrecep, (nrows)))
1092
+ tbl.putcol("BEAM_ID", np.broadcast_to(-1, (nrows)))
1093
+ boff = np.deg2rad([0.1, 0.3])
1094
+ tbl.putcol("BEAM_OFFSET", np.broadcast_to(boff, (nrows, 2, nrecep)))
1095
+ pol_types = ["test_X, test_Y"]
1096
+ len_pol_types = 2
1097
+ tbl.putcol(
1098
+ "POLARIZATION_TYPE", np.broadcast_to(pol_types, (nrows, len_pol_types))
1099
+ )
1100
+ tbl.putcol("POL_RESPONSE", np.broadcast_to(0.0, (nrows, 2, nrecep)))
1101
+ tbl.putcol("POSITION", np.broadcast_to([0.0, 0.0, 0.0], (nrows, 3)))
1102
+ tbl.putcol("RECEPTOR_ANGLE", np.broadcast_to([1.51, 0.33], (nrows, nrecep)))
1103
+
1104
+
1105
+ def gen_subt_observation(mspath: str, obs_descr: dict):
1106
+ """
1107
+ Populate the OBSERVATION table, with one row per observation id/number
1108
+ (MSv2: the id is implicitly the row number).
1109
+ The string columns are filled with values loosely based on example ALMA
1110
+ MSs.
1111
+
1112
+ Parameters
1113
+ ----------
1114
+ mspath : str
1115
+ path of output MS
1116
+ obs_descr : dict
1117
+ obs description, with IDs, only the len is considered
1118
+
1119
+ Returns
1120
+ -------
1121
+
1122
+ """
1123
+
1124
+ nobs = len(obs_descr)
1125
+ with tables.table(mspath + "::OBSERVATION", ack=False, readonly=False) as tbl:
1126
+ tbl.addrows(nobs)
1127
+ # no keys, all data cols:
1128
+ # "test_telescope" would produce errors for example in listobs:
1129
+ # Exception: Telescope test_telescope is not recognized by CASA.
1130
+ # (casa6core::MPosition casa6core::MSMetaData::getObservatoryPosition())
1131
+ tbl.putcol("TELESCOPE_NAME", np.repeat("ALMA", nobs))
1132
+ tbl.putcol("TIME_RANGE", np.broadcast_to([[4.87021e9, 4.87022e9]], (nobs, 2)))
1133
+ tbl.putcol("OBSERVER", np.repeat("Dr. test_observer", nobs))
1134
+ tbl.putcol(
1135
+ "LOG", np.broadcast_to(["test_obs_log_1", "test_obs_log_2"], (nobs, 2))
1136
+ )
1137
+ tbl.putcol("SCHEDULE_TYPE", np.repeat("test_schedule_type", nobs))
1138
+ sched = np.array(
1139
+ [
1140
+ [
1141
+ f"SchedulingBlock uid://A002/X1ftest/Xde{idx}",
1142
+ f"ExecBlock uid://A002/X1abtest/X123{idx}",
1143
+ ]
1144
+ for idx in range(nobs)
1145
+ ]
1146
+ )
1147
+ tbl.putcol("SCHEDULE", sched)
1148
+ proj = np.array([f"uid://A002/X1ftest/X4ec{idx}" for idx in range(nobs)])
1149
+ tbl.putcol("PROJECT", proj)
1150
+ tbl.putcol("RELEASE_DATE", np.repeat(0, nobs))
1151
+
1152
+
1153
+ def gen_subt_processor(mspath: str, proc_descr: dict):
1154
+ """
1155
+ Populate the PROCESSOR table, with one row per processor id/number
1156
+ (MSv2: the id is implicitly the row number). The TYPE_ID is left to -1.
1157
+ All values of TYPE are CORRELATOR.
1158
+ MODE_ID are also left to -1 (no ..._MODE subtable).
1159
+ The string columns are filled with values loosely based on example ALMA
1160
+ MSs.
1161
+
1162
+ Parameters
1163
+ ----------
1164
+ mspath : str
1165
+ path of output MS
1166
+ proc_descr : dict
1167
+ processors description, with IDs, only the len is considered
1168
+
1169
+ Returns
1170
+ -------
1171
+
1172
+ """
1173
+
1174
+ nproc = len(proc_descr)
1175
+ with tables.table(mspath + "::PROCESSOR", ack=False, readonly=False) as tbl:
1176
+ tbl.addrows(nproc)
1177
+ # no keys, all data cols:
1178
+ # There could also be RADIOMETER, SPECTROMETER, PULSAR-TIMER, etc. - not nor now
1179
+ tbl.putcol("TYPE", np.repeat("CORRELATOR", nproc))
1180
+ tbl.putcol("SUB_TYPE", np.repeat("test_CORRELATOR_MODE", nproc))
1181
+ tbl.putcol("TYPE_ID", np.repeat(-1, nproc))
1182
+ tbl.putcol("MODE_ID", np.repeat(-1, nproc))
1183
+ tbl.putcol("FLAG_ROW", np.repeat(False, nproc))
1184
+
1185
+
1186
+ def gen_subt_flag_cmd(mspath: str):
1187
+ """
1188
+ Leaves the FLAG_CMD subtable empty for now, and checks that there are no
1189
+ rows.
1190
+ """
1191
+ with tables.table(mspath + "::FLAG_CMD", ack=False, readonly=False) as tbl:
1192
+ assert tbl.nrows() == 0
1193
+
1194
+
1195
+ def gen_subt_history(mspath: str, obs_descr: dict):
1196
+ """
1197
+ Populate the HISTORY table, with only one row per observation
1198
+ The string columns are filled with values loosely based on example ALMA MSs.
1199
+ OBJECT_ID is left all to -1.
1200
+
1201
+ Parameters
1202
+ ----------
1203
+ mspath : str
1204
+ path of output MS
1205
+ obs_descr : dict
1206
+ obs description, with IDs, only the len is considered
1207
+
1208
+ Returns
1209
+ -------
1210
+
1211
+ """
1212
+
1213
+ nobs = len(obs_descr)
1214
+ with tables.table(mspath + "::HISTORY", ack=False, readonly=False) as tbl:
1215
+ tbl.addrows(nobs)
1216
+ # keys
1217
+ tbl.putcol("TIME", np.repeat(0, nobs))
1218
+ tbl.putcol("OBSERVATION_ID", np.arange(nobs))
1219
+ # data
1220
+ tbl.putcol("MESSAGE", np.repeat("made with test ms generator", nobs))
1221
+ tbl.putcol("PRIORITY", np.repeat("INFO", nobs))
1222
+ tbl.putcol("ORIGIN", np.repeat("ms_maker", nobs))
1223
+ tbl.putcol("OBJECT_ID", np.repeat(0, nobs))
1224
+ tbl.putcol("APPLICATION", np.repeat("xradio", nobs))
1225
+ tbl.putcol("CLI_COMMAND", np.broadcast_to("gen_subt_history", (nobs, 1)))
1226
+ tbl.putcol("APP_PARAMS", np.broadcast_to("", (nobs, 1)))
1227
+
1228
+
1229
+ def gen_subt_syscal(mspath: str, ant_descr: dict):
1230
+ """
1231
+ Creates a SYSCAL subtable and populates it with a (very incomplete) row
1232
+ This is just to enable minimal coverage of some SYSCAL handling code in
1233
+ the casacore tables read/write functions.
1234
+ """
1235
+ ncal = len(ant_descr)
1236
+ subt_name = "SYSCAL"
1237
+ with open_opt_subtable(mspath, subt_name) as sctbl:
1238
+ sctbl.addrows(ncal)
1239
+ sctbl.putcol("ANTENNA_ID", np.arange(0, ncal))
1240
+ sctbl.putcol("FEED_ID", np.repeat(0, ncal))
1241
+ sctbl.putcol("SPECTRAL_WINDOW_ID", np.repeat(0, ncal))
1242
+ sctbl.putcol("TIME", np.repeat(1e10, ncal))
1243
+ sctbl.putcol("INTERVAL", np.repeat(1e12, ncal))
1244
+ # all data/flags columns in the SYSCAL table are optional!
1245
+ # sctbl.putcol("PHASE_DIFF", np.repeat(0.3, ncal))
1246
+ sctbl.putcol("PHASE_DIFF", np.repeat(0.3, ncal))
1247
+ # sctbl.putcol("TCAL", np.broadcast_to(50.3, (ncal, 2)))
1248
+ sctbl.putcol("TCAL_SPECTRUM", np.broadcast_to(50.3, (ncal, 10, 2)))
1249
+ # TRX, etc.
1250
+
1251
+ with tables.table(mspath, ack=False, readonly=False) as main:
1252
+ main.putkeyword(subt_name, f"Table: {mspath}/{subt_name}")
1253
+
1254
+
1255
+ def gen_subt_weather(mspath: str, misbehave: bool = False):
1256
+ """
1257
+ Creates a WEATHER subtable and populates it with a (very incomplete) row
1258
+ simply with a very high TIME value, as seen in some corner cases of
1259
+ test MSs.
1260
+ This is just to enable minimal coverage of some WEATHER handling code in
1261
+ the casacore tables read/write functions.
1262
+ """
1263
+ subt_name = "WEATHER"
1264
+ tabdesc_ns_wx_station = {
1265
+ "NS_WX_STATION_ID": {
1266
+ "valueType": "int",
1267
+ "dataManagerType": "StandardStMan",
1268
+ "dataManagerGroup": "StandardStMan",
1269
+ "option": 0,
1270
+ "maxlen": 0,
1271
+ "comment": "comment...",
1272
+ "keywords": {},
1273
+ },
1274
+ "NS_WX_STATION_POSITION": {
1275
+ "valueType": "double",
1276
+ "dataManagerType": "StandardStMan",
1277
+ "dataManagerGroup": "StandardStMan",
1278
+ "option": 0,
1279
+ "maxlen": 0,
1280
+ "ndim": 1,
1281
+ "comment": "comment...",
1282
+ "keywords": {},
1283
+ },
1284
+ }
1285
+ nrows = 5
1286
+ with open_opt_subtable(mspath, subt_name) as wtbl:
1287
+ wtbl.addrows(nrows)
1288
+ wtbl.putcol("ANTENNA_ID", np.arange(0, nrows))
1289
+ wtbl.putcol("TIME", np.arange(0, nrows) * 100 + 1e12)
1290
+ wtbl.putcol("INTERVAL", np.repeat(1e12, nrows))
1291
+ # all data/flags columns in the WEATHER table are optional!
1292
+ # But note they are always added in the python-casacore defaults
1293
+ wtbl.putcol("H2O", np.repeat(0.03, nrows))
1294
+ if not misbehave:
1295
+ wtbl.addcols(tabdesc_ns_wx_station)
1296
+ wtbl.putcol("ANTENNA_ID", np.repeat(-1, nrows))
1297
+ wtbl.putcol("NS_WX_STATION_ID", np.arange(0, nrows))
1298
+ wtbl.putcol(
1299
+ "NS_WX_STATION_POSITION", np.broadcast_to([0.1, 0.2, 0.3], (nrows, 3))
1300
+ )
1301
+
1302
+ with tables.table(mspath, ack=False, readonly=False) as main:
1303
+ main.putkeyword(subt_name, f"Table: {mspath}/{subt_name}")
1304
+
1305
+
1306
+ def gen_subt_asdm_receiver(mspath: str):
1307
+ """
1308
+ Produces an over-simple table, with only one row, for basic coverage of ASDM_* subtables handling
1309
+ code.
1310
+ """
1311
+
1312
+ subt_name = "ASDM_RECEIVER"
1313
+ rec_path = Path(mspath) / subt_name
1314
+ tabdesc = {
1315
+ "receiverId": {
1316
+ "valueType": "int",
1317
+ "dataManagerType": "StandardStMan",
1318
+ "dataManagerGroup": "StandardStMan",
1319
+ "option": 0,
1320
+ "maxlen": 0,
1321
+ "comment": "comment...",
1322
+ "keywords": {},
1323
+ },
1324
+ "spectralWindowId": {
1325
+ "valueType": "int",
1326
+ "dataManagerType": "StandardStMan",
1327
+ "dataManagerGroup": "StandardStMan",
1328
+ "option": 0,
1329
+ "maxlen": 0,
1330
+ "comment": "comment...",
1331
+ "keywords": {},
1332
+ },
1333
+ "timeInterval": {
1334
+ "valueType": "double",
1335
+ "dataManagerType": "StandardStMan",
1336
+ "dataManagerGroup": "StandardStMan",
1337
+ "option": 0,
1338
+ "maxlen": 0,
1339
+ "comment": "comment...",
1340
+ "keywords": {},
1341
+ },
1342
+ }
1343
+
1344
+ with tables.table(
1345
+ str(rec_path), tabledesc=tabdesc, nrow=1, readonly=False, ack=False
1346
+ ) as tbl:
1347
+ tbl.putcol("receiverId", 0)
1348
+ tbl.putcol("spectralWindowId", 0)
1349
+ tbl.putcol("timeInterval", 0)
1350
+
1351
+ with tables.table(mspath, ack=False, readonly=False) as main:
1352
+ main.putkeyword(subt_name, f"Table: {mspath}/{subt_name}")
1353
+
1354
+
1355
+ def gen_subt_asdm_station(mspath: str):
1356
+ """
1357
+ Produces an over-simple table, with only a few rows, for basic coverage of ASDM_STATION subtable
1358
+ handling (relevant when loading WEATHER)
1359
+ """
1360
+
1361
+ subt_name = "ASDM_STATION"
1362
+ rec_path = Path(mspath) / subt_name
1363
+ tabdesc = {
1364
+ "stationId": {
1365
+ "valueType": "int",
1366
+ "dataManagerType": "StandardStMan",
1367
+ "dataManagerGroup": "StandardStMan",
1368
+ "option": 0,
1369
+ "maxlen": 0,
1370
+ "comment": "comment...",
1371
+ "keywords": {},
1372
+ },
1373
+ "name": {
1374
+ "valueType": "string",
1375
+ "dataManagerType": "StandardStMan",
1376
+ "dataManagerGroup": "StandardStMan",
1377
+ "option": 0,
1378
+ "maxlen": 0,
1379
+ "comment": "comment...",
1380
+ "keywords": {},
1381
+ },
1382
+ "position": {
1383
+ "valueType": "double",
1384
+ "dataManagerType": "StandardStMan",
1385
+ "dataManagerGroup": "StandardStMan",
1386
+ "option": 0,
1387
+ "maxlen": 0,
1388
+ "ndim": 1,
1389
+ "comment": "comment...",
1390
+ "keywords": {},
1391
+ },
1392
+ "type": {
1393
+ "valueType": "string",
1394
+ "dataManagerType": "StandardStMan",
1395
+ "dataManagerGroup": "StandardStMan",
1396
+ "option": 0,
1397
+ "maxlen": 0,
1398
+ "comment": "comment...",
1399
+ "keywords": {},
1400
+ },
1401
+ "time": {
1402
+ "valueType": "double",
1403
+ "dataManagerType": "StandardStMan",
1404
+ "dataManagerGroup": "StandardStMan",
1405
+ "option": 0,
1406
+ "maxlen": 0,
1407
+ "comment": "comment...",
1408
+ "keywords": {},
1409
+ },
1410
+ }
1411
+
1412
+ nrows = 5
1413
+ with tables.table(
1414
+ str(rec_path), tabledesc=tabdesc, nrow=nrows, readonly=False, ack=False
1415
+ ) as tbl:
1416
+ tbl.putcol("stationId", np.arange(0, nrows))
1417
+ tbl.putcol("position", np.broadcast_to([10, 20, 30], (nrows, 3)))
1418
+ tbl.putcol("name", [f"test_station{idx}" for idx in np.arange(0, nrows)])
1419
+ tbl.putcol(
1420
+ "type", np.repeat("WEATHER_STATION", nrows)
1421
+ ) # ANTENNA_PAD / MAINTENANCE_PAD
1422
+ tbl.putcol("time", np.repeat(1e9, nrows))
1423
+
1424
+ with tables.table(mspath, ack=False, readonly=False) as main:
1425
+ main.putkeyword(subt_name, f"Table: {mspath}/{subt_name}")
1426
+
1427
+
1428
+ def gen_subt_asdm_execblock(mspath: str):
1429
+ """
1430
+ Produces a basic ASDM/EXECBLOCK table, for basic coverage of code that handles the ASDM_*
1431
+ subtables.
1432
+ For now it simply creates a table with one row
1433
+ """
1434
+
1435
+ subt_name = "ASDM_EXECBLOCK"
1436
+ rec_path = Path(mspath) / subt_name
1437
+ tabdesc = {
1438
+ "execBlockIDId": {
1439
+ "valueType": "string",
1440
+ "dataManagerType": "StandardStMan",
1441
+ "dataManagerGroup": "StandardStMan",
1442
+ "option": 0,
1443
+ "maxlen": 0,
1444
+ "comment": "comment...",
1445
+ "keywords": {},
1446
+ },
1447
+ "execBlockNumId": {
1448
+ "valueType": "string",
1449
+ "dataManagerType": "StandardStMan",
1450
+ "dataManagerGroup": "StandardStMan",
1451
+ "option": 0,
1452
+ "maxlen": 0,
1453
+ "comment": "comment...",
1454
+ "keywords": {},
1455
+ },
1456
+ "execBlockUID": {
1457
+ "valueType": "string",
1458
+ "dataManagerType": "StandardStMan",
1459
+ "dataManagerGroup": "StandardStMan",
1460
+ "option": 0,
1461
+ "maxlen": 0,
1462
+ "comment": "comment...",
1463
+ "keywords": {},
1464
+ },
1465
+ "sessionReference": {
1466
+ "valueType": "string",
1467
+ "dataManagerType": "StandardStMan",
1468
+ "dataManagerGroup": "StandardStMan",
1469
+ "option": 0,
1470
+ "maxlen": 0,
1471
+ "comment": "comment...",
1472
+ "keywords": {},
1473
+ },
1474
+ "observingScript": {
1475
+ "valueType": "string",
1476
+ "dataManagerType": "StandardStMan",
1477
+ "dataManagerGroup": "StandardStMan",
1478
+ "option": 0,
1479
+ "maxlen": 0,
1480
+ "comment": "comment...",
1481
+ "keywords": {},
1482
+ },
1483
+ "observingScriptUID": {
1484
+ "valueType": "string",
1485
+ "dataManagerType": "StandardStMan",
1486
+ "dataManagerGroup": "StandardStMan",
1487
+ "option": 0,
1488
+ "maxlen": 0,
1489
+ "comment": "comment...",
1490
+ "keywords": {},
1491
+ },
1492
+ "observingLog": {
1493
+ "valueType": "string",
1494
+ "dataManagerType": "StandardStMan",
1495
+ "dataManagerGroup": "StandardStMan",
1496
+ "option": 0,
1497
+ "maxlen": 0,
1498
+ "comment": "comment...",
1499
+ "ndim": 1,
1500
+ "_c_order": True,
1501
+ "keywords": {},
1502
+ },
1503
+ }
1504
+
1505
+ nrows = 1
1506
+ with tables.table(
1507
+ str(rec_path), tabledesc=tabdesc, nrow=nrows, readonly=False, ack=False
1508
+ ) as tbl:
1509
+ tbl.putcol("execBlockIDId", "1")
1510
+ tbl.putcol("execBlockNumId", "3")
1511
+ tbl.putcol("execBlockUID", "uid://A001/X1abtest/X123")
1512
+ tbl.putcol("sessionReference", "test_session_ref")
1513
+ tbl.putcol("observingScript", "test script")
1514
+ tbl.putcol("observingScriptUID", "uid://A003/X1abtest/X987")
1515
+ tbl.putcol("observingLog", np.broadcast_to("test log line 0", (nrows, 1)))
1516
+ # tbl.putcol("observingLog", np.broadcast_to(np.array(["test log line 0", "test log line 1"], dtype="str"), (nrows, 2)))
1517
+
1518
+ with tables.table(mspath, ack=False, readonly=False) as main:
1519
+ main.putkeyword(subt_name, f"Table: {mspath}/{subt_name}")
1520
+
1521
+
1522
+ def gen_subt_gain_curve(mspath: str, ant_descr: dict):
1523
+ """
1524
+ Produces a basic GAIN_CURVE (sub)table, following casacore note #265, for basic coverage of
1525
+ VLBI subtables handling.
1526
+ """
1527
+
1528
+ subt_name = "GAIN_CURVE"
1529
+ rec_path = Path(mspath) / subt_name
1530
+ tabdesc = {
1531
+ "ANTENNA_ID": {
1532
+ "valueType": "int",
1533
+ "dataManagerType": "StandardStMan",
1534
+ "dataManagerGroup": "StandardStMan",
1535
+ "option": 0,
1536
+ "maxlen": 0,
1537
+ "comment": "comment...",
1538
+ "keywords": {},
1539
+ },
1540
+ "FEED_ID": {
1541
+ "valueType": "int",
1542
+ "dataManagerType": "StandardStMan",
1543
+ "dataManagerGroup": "StandardStMan",
1544
+ "option": 0,
1545
+ "maxlen": 0,
1546
+ "comment": "comment...",
1547
+ "keywords": {},
1548
+ },
1549
+ "SPECTRAL_WINDOW_ID": {
1550
+ "valueType": "int",
1551
+ "dataManagerType": "StandardStMan",
1552
+ "dataManagerGroup": "StandardStMan",
1553
+ "option": 0,
1554
+ "maxlen": 0,
1555
+ "comment": "comment...",
1556
+ "keywords": {},
1557
+ },
1558
+ "TIME": {
1559
+ "valueType": "int",
1560
+ "dataManagerType": "StandardStMan",
1561
+ "dataManagerGroup": "StandardStMan",
1562
+ "option": 0,
1563
+ "maxlen": 0,
1564
+ "comment": "comment...",
1565
+ "keywords": {
1566
+ "UNIT": "s",
1567
+ },
1568
+ },
1569
+ "INTERVAL": {
1570
+ "valueType": "int",
1571
+ "dataManagerType": "StandardStMan",
1572
+ "dataManagerGroup": "StandardStMan",
1573
+ "option": 0,
1574
+ "maxlen": 0,
1575
+ "comment": "comment...",
1576
+ "keywords": {
1577
+ "UNIT": "s",
1578
+ },
1579
+ },
1580
+ "TYPE": {
1581
+ "valueType": "string",
1582
+ "dataManagerType": "StandardStMan",
1583
+ "dataManagerGroup": "StandardStMan",
1584
+ "option": 0,
1585
+ "maxlen": 0,
1586
+ "comment": "comment...",
1587
+ "keywords": {},
1588
+ },
1589
+ "NUM_POLY": {
1590
+ "valueType": "int",
1591
+ "dataManagerType": "StandardStMan",
1592
+ "dataManagerGroup": "StandardStMan",
1593
+ "option": 0,
1594
+ "maxlen": 0,
1595
+ "comment": "comment...",
1596
+ "keywords": {},
1597
+ },
1598
+ "GAIN": {
1599
+ "valueType": "double",
1600
+ "dataManagerType": "StandardStMan",
1601
+ "dataManagerGroup": "StandardStMan",
1602
+ "option": 0,
1603
+ "maxlen": 0,
1604
+ "ndim": 2,
1605
+ # "shape"
1606
+ "comment": "comment...",
1607
+ "keywords": {},
1608
+ },
1609
+ "SENSITIVITY": {
1610
+ "valueType": "double",
1611
+ "dataManagerType": "StandardStMan",
1612
+ "dataManagerGroup": "StandardStMan",
1613
+ "option": 0,
1614
+ "maxlen": 0,
1615
+ "ndim": 1,
1616
+ # "shape"
1617
+ "comment": "comment...",
1618
+ "keywords": {
1619
+ "UNIT": "K/Jy",
1620
+ },
1621
+ },
1622
+ }
1623
+
1624
+ nants = len(ant_descr)
1625
+ nreceptors = 2
1626
+ num_poly = 2
1627
+ with tables.table(
1628
+ str(rec_path), tabledesc=tabdesc, nrow=nants, readonly=False, ack=False
1629
+ ) as tbl:
1630
+ tbl.putcol("ANTENNA_ID", np.arange(0, nants))
1631
+ tbl.putcol("FEED_ID", np.repeat(0, nants))
1632
+ tbl.putcol("SPECTRAL_WINDOW_ID", np.repeat(0, nants))
1633
+ tbl.putcol("TIME", np.repeat(1e12, nants))
1634
+ tbl.putcol("INTERVAL", np.repeat(2, nants))
1635
+ tbl.putcol("TYPE", np.repeat("(”POWER(EL)", nants))
1636
+ tbl.putcol("NUM_POLY", np.repeat(num_poly, nants))
1637
+ tbl.putcol("GAIN", np.broadcast_to(0.85, (nants, nreceptors, num_poly)))
1638
+ tbl.putcol("SENSITIVITY", np.broadcast_to(0.95, (nants, nreceptors)))
1639
+
1640
+ with tables.table(mspath, ack=False, readonly=False) as main:
1641
+ main.putkeyword(subt_name, f"Table: {mspath}/{subt_name}")
1642
+
1643
+
1644
+ def gen_subt_phase_cal(mspath: str, ant_descr: dict):
1645
+ """
1646
+ Produces a basic PHASE_CAL (sub)table, following casacore note #265, for basic coverage of
1647
+ VLBI subtables handling.
1648
+ Note some differences in example test datasets like VLBA_TL016B_split.ms dimensions with respect
1649
+ to the casacore note tables.
1650
+ """
1651
+
1652
+ subt_name = "PHASE_CAL"
1653
+ rec_path = Path(mspath) / subt_name
1654
+ tabdesc = {
1655
+ "ANTENNA_ID": {
1656
+ "valueType": "int",
1657
+ "dataManagerType": "StandardStMan",
1658
+ "dataManagerGroup": "StandardStMan",
1659
+ "option": 0,
1660
+ "maxlen": 0,
1661
+ "comment": "comment...",
1662
+ "keywords": {},
1663
+ },
1664
+ "FEED_ID": {
1665
+ "valueType": "int",
1666
+ "dataManagerType": "StandardStMan",
1667
+ "dataManagerGroup": "StandardStMan",
1668
+ "option": 0,
1669
+ "maxlen": 0,
1670
+ "comment": "comment...",
1671
+ "keywords": {},
1672
+ },
1673
+ "SPECTRAL_WINDOW_ID": {
1674
+ "valueType": "int",
1675
+ "dataManagerType": "StandardStMan",
1676
+ "dataManagerGroup": "StandardStMan",
1677
+ "option": 0,
1678
+ "maxlen": 0,
1679
+ "comment": "comment...",
1680
+ "keywords": {},
1681
+ },
1682
+ "TIME": {
1683
+ "valueType": "int",
1684
+ "dataManagerType": "StandardStMan",
1685
+ "dataManagerGroup": "StandardStMan",
1686
+ "option": 0,
1687
+ "maxlen": 0,
1688
+ "comment": "comment...",
1689
+ "keywords": {
1690
+ "UNIT": "s",
1691
+ },
1692
+ },
1693
+ "INTERVAL": {
1694
+ "valueType": "int",
1695
+ "dataManagerType": "StandardStMan",
1696
+ "dataManagerGroup": "StandardStMan",
1697
+ "option": 0,
1698
+ "maxlen": 0,
1699
+ "comment": "comment...",
1700
+ "keywords": {
1701
+ "UNIT": "s",
1702
+ },
1703
+ },
1704
+ "NUM_TONES": {
1705
+ "valueType": "int",
1706
+ "dataManagerType": "StandardStMan",
1707
+ "dataManagerGroup": "StandardStMan",
1708
+ "option": 0,
1709
+ "maxlen": 0,
1710
+ "comment": "comment...",
1711
+ "keywords": {},
1712
+ },
1713
+ "TONE_FREQUENCY": {
1714
+ "valueType": "double",
1715
+ "dataManagerType": "StandardStMan",
1716
+ "dataManagerGroup": "StandardStMan",
1717
+ "option": 0,
1718
+ "maxlen": 0,
1719
+ "ndim": 2,
1720
+ # "shape":
1721
+ "comment": "comment...",
1722
+ "keywords": {
1723
+ "QuantumUnits": ["Hz"],
1724
+ "MEASINFO": {"type": "frequency", "Ref": "bogus ref frame"},
1725
+ },
1726
+ },
1727
+ "PHASE_CAL": {
1728
+ "valueType": "double",
1729
+ "dataManagerType": "StandardStMan",
1730
+ "dataManagerGroup": "StandardStMan",
1731
+ "option": 0,
1732
+ "maxlen": 0,
1733
+ "ndim": 2,
1734
+ # "shape"
1735
+ "comment": "comment...",
1736
+ "keywords": {},
1737
+ },
1738
+ "CABLE_CAL": {
1739
+ "valueType": "double",
1740
+ "dataManagerType": "StandardStMan",
1741
+ "dataManagerGroup": "StandardStMan",
1742
+ "option": 0,
1743
+ "maxlen": 0,
1744
+ # "ndim": 1,
1745
+ # "shape"
1746
+ "comment": "comment...",
1747
+ "keywords": {
1748
+ "QuantumUnits": "s",
1749
+ },
1750
+ },
1751
+ }
1752
+
1753
+ nants = len(ant_descr)
1754
+ nreceptors = 2
1755
+ ntones = 3
1756
+ _ntimes = 1
1757
+ with tables.table(
1758
+ str(rec_path), tabledesc=tabdesc, nrow=nants, readonly=False, ack=False
1759
+ ) as tbl:
1760
+ tbl.putcol("ANTENNA_ID", np.arange(0, nants))
1761
+ tbl.putcol("FEED_ID", np.repeat(0, nants))
1762
+ tbl.putcol("SPECTRAL_WINDOW_ID", np.repeat(0, nants))
1763
+ tbl.putcol("TIME", np.repeat(1e12, nants))
1764
+ tbl.putcol("INTERVAL", np.repeat(2, nants))
1765
+ tbl.putcol("NUM_TONES", np.repeat(ntones, nants))
1766
+ tbl.putcol(
1767
+ "TONE_FREQUENCY", np.broadcast_to(1.234e9, (nants, ntones, nreceptors))
1768
+ )
1769
+ tbl.putcol("PHASE_CAL", np.broadcast_to(0.92, (nants, ntones, nreceptors)))
1770
+ tbl.putcol("CABLE_CAL", np.repeat(0.93, (nants)))
1771
+
1772
+ with tables.table(mspath, ack=False, readonly=False) as main:
1773
+ main.putkeyword(subt_name, f"Table: {mspath}/{subt_name}")
1774
+
1775
+
1776
+ # Functions from io.py that depend on casacore
1777
+
1778
+
1779
+ def build_processing_set_from_msv2(
1780
+ in_file: str | Path,
1781
+ out_file: str | Path,
1782
+ *,
1783
+ partition_scheme: Optional[Iterable[dict]] = None,
1784
+ persistence_mode: str = "w",
1785
+ parallel_mode: str = "partition",
1786
+ **convert_kwargs: Any,
1787
+ ) -> Path:
1788
+ """
1789
+ Convert an MSv2 dataset into a processing set using the production converter.
1790
+ """
1791
+ from xradio.measurement_set import convert_msv2_to_processing_set
1792
+
1793
+ convert_msv2_to_processing_set(
1794
+ in_file=str(in_file),
1795
+ out_file=str(out_file),
1796
+ partition_scheme=list(partition_scheme or []),
1797
+ persistence_mode=persistence_mode,
1798
+ parallel_mode=parallel_mode,
1799
+ **convert_kwargs,
1800
+ )
1801
+ return Path(out_file)
1802
+
1803
+
1804
+ def build_msv4_partition(
1805
+ ms_path: str | Path,
1806
+ out_root: str | Path,
1807
+ *,
1808
+ msv4_id: str = "msv4id",
1809
+ partition_kwargs: Optional[Dict[str, Any]] = None,
1810
+ use_table_iter: bool = False,
1811
+ persistence_mode: str = "w",
1812
+ ) -> Path:
1813
+ """
1814
+ Convert a MeasurementSet v2 partition into an MSv4 Zarr tree.
1815
+ """
1816
+ from xradio.measurement_set._utils._msv2.conversion import (
1817
+ convert_and_write_partition,
1818
+ )
1819
+
1820
+ partition_kwargs = partition_kwargs or {"DATA_DESC_ID": [0]}
1821
+ convert_and_write_partition(
1822
+ str(ms_path),
1823
+ str(out_root),
1824
+ msv4_id,
1825
+ partition_kwargs,
1826
+ use_table_iter=use_table_iter,
1827
+ persistence_mode=persistence_mode,
1828
+ )
1829
+ return Path(out_root) / f"{Path(ms_path).stem}_{msv4_id}"
1830
+
1831
+
1832
+ def build_minimal_msv4_xdt(
1833
+ ms_path: str | Path,
1834
+ *,
1835
+ out_root: str | Path | None = None,
1836
+ msv4_id: str = "msv4id",
1837
+ partition_kwargs: Optional[Dict[str, Any]] = None,
1838
+ use_table_iter: bool = False,
1839
+ persistence_mode: str = "w",
1840
+ ) -> Path:
1841
+ """
1842
+ Convenience wrapper that selects reasonable defaults for the minimal MSv4 conversion.
1843
+ """
1844
+
1845
+ if out_root is None:
1846
+ out_root = Path(f"{Path(ms_path).stem}_processing_set.zarr")
1847
+ return build_msv4_partition(
1848
+ ms_path,
1849
+ out_root,
1850
+ msv4_id=msv4_id,
1851
+ partition_kwargs=partition_kwargs,
1852
+ use_table_iter=use_table_iter,
1853
+ persistence_mode=persistence_mode,
1854
+ )