xradio 1.0.2__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.
- xradio/_utils/_casacore/casacore_from_casatools.py +1 -1
- xradio/_utils/dict_helpers.py +38 -7
- xradio/_utils/list_and_array.py +26 -3
- xradio/_utils/schema.py +44 -0
- xradio/_utils/xarray_helpers.py +63 -0
- xradio/_utils/zarr/common.py +4 -2
- xradio/image/__init__.py +4 -2
- xradio/image/_util/_casacore/common.py +2 -1
- xradio/image/_util/_casacore/xds_from_casacore.py +105 -51
- xradio/image/_util/_casacore/xds_to_casacore.py +117 -52
- xradio/image/_util/_fits/xds_from_fits.py +124 -36
- xradio/image/_util/_zarr/common.py +0 -1
- xradio/image/_util/casacore.py +133 -16
- xradio/image/_util/common.py +6 -5
- xradio/image/_util/image_factory.py +466 -27
- xradio/image/image.py +72 -100
- xradio/image/image_xds.py +262 -0
- xradio/image/schema.py +85 -0
- xradio/measurement_set/__init__.py +5 -4
- xradio/measurement_set/_utils/_msv2/_tables/read.py +7 -3
- xradio/measurement_set/_utils/_msv2/conversion.py +6 -9
- xradio/measurement_set/_utils/_msv2/create_field_and_source_xds.py +1 -0
- xradio/measurement_set/_utils/_msv2/msv4_sub_xdss.py +1 -1
- xradio/measurement_set/_utils/_utils/interpolate.py +5 -0
- xradio/measurement_set/_utils/_utils/partition_attrs.py +0 -1
- xradio/measurement_set/convert_msv2_to_processing_set.py +9 -9
- xradio/measurement_set/load_processing_set.py +2 -2
- xradio/measurement_set/measurement_set_xdt.py +83 -93
- xradio/measurement_set/open_processing_set.py +1 -1
- xradio/measurement_set/processing_set_xdt.py +33 -26
- xradio/schema/check.py +70 -19
- xradio/schema/common.py +0 -1
- xradio/testing/__init__.py +0 -0
- xradio/testing/_utils/__template__.py +58 -0
- xradio/testing/measurement_set/__init__.py +58 -0
- xradio/testing/measurement_set/checker.py +131 -0
- xradio/testing/measurement_set/io.py +22 -0
- xradio/testing/measurement_set/msv2_io.py +1854 -0
- {xradio-1.0.2.dist-info → xradio-1.1.0.dist-info}/METADATA +64 -23
- xradio-1.1.0.dist-info/RECORD +75 -0
- {xradio-1.0.2.dist-info → xradio-1.1.0.dist-info}/WHEEL +1 -1
- xradio-1.0.2.dist-info/RECORD +0 -66
- {xradio-1.0.2.dist-info → xradio-1.1.0.dist-info}/licenses/LICENSE.txt +0 -0
- {xradio-1.0.2.dist-info → xradio-1.1.0.dist-info}/top_level.txt +0 -0
xradio/schema/check.py
CHANGED
|
@@ -7,7 +7,6 @@ import warnings
|
|
|
7
7
|
|
|
8
8
|
import xarray
|
|
9
9
|
import numpy
|
|
10
|
-
from typeguard import check_type, TypeCheckError
|
|
11
10
|
|
|
12
11
|
from xradio.schema import (
|
|
13
12
|
metamodel,
|
|
@@ -502,38 +501,90 @@ def _check_value(val: typing.Any, schema: metamodel.ValueSchema):
|
|
|
502
501
|
]
|
|
503
502
|
)
|
|
504
503
|
|
|
505
|
-
if
|
|
506
|
-
# Fall through to plain type check
|
|
507
|
-
type_to_check = xarray.DataArray
|
|
508
|
-
else:
|
|
504
|
+
if isinstance(val, xarray.DataArray):
|
|
509
505
|
return check_array(val, schema.array_schema)
|
|
510
506
|
|
|
507
|
+
expected = [xarray.DataArray]
|
|
508
|
+
if schema.optional:
|
|
509
|
+
expected.append(type(None))
|
|
510
|
+
return SchemaIssues(
|
|
511
|
+
[
|
|
512
|
+
SchemaIssue(
|
|
513
|
+
path=[],
|
|
514
|
+
message=f"{type(val).__name__} is not an xarray.DataArray!",
|
|
515
|
+
expected=expected,
|
|
516
|
+
found=type(val),
|
|
517
|
+
)
|
|
518
|
+
]
|
|
519
|
+
)
|
|
520
|
+
|
|
511
521
|
# Is supposed to be a dictionary?
|
|
512
522
|
elif schema.type == "dict":
|
|
513
523
|
if not isinstance(val, dict):
|
|
514
|
-
|
|
515
|
-
|
|
524
|
+
expected = [dict]
|
|
525
|
+
if schema.optional:
|
|
526
|
+
expected.append(type(None))
|
|
527
|
+
return SchemaIssues(
|
|
528
|
+
[
|
|
529
|
+
SchemaIssue(
|
|
530
|
+
path=[],
|
|
531
|
+
message=f"{type(val).__name__} is not a dictionary!",
|
|
532
|
+
expected=expected,
|
|
533
|
+
found=type(val),
|
|
534
|
+
)
|
|
535
|
+
]
|
|
536
|
+
)
|
|
516
537
|
else:
|
|
517
538
|
return check_dict(val, schema.dict_schema)
|
|
518
539
|
|
|
519
540
|
elif schema.type == "list[str]":
|
|
520
|
-
|
|
541
|
+
if not isinstance(val, list):
|
|
542
|
+
expected = [list[str]]
|
|
543
|
+
if schema.optional:
|
|
544
|
+
expected.append(type(None))
|
|
545
|
+
return SchemaIssues(
|
|
546
|
+
[
|
|
547
|
+
SchemaIssue(
|
|
548
|
+
path=[],
|
|
549
|
+
message=f"{type(val).__name__} is not a list!",
|
|
550
|
+
expected=expected,
|
|
551
|
+
found=type(val),
|
|
552
|
+
)
|
|
553
|
+
]
|
|
554
|
+
)
|
|
555
|
+
if any(type(v) is not str for v in val):
|
|
556
|
+
expected = [list[str]]
|
|
557
|
+
if schema.optional:
|
|
558
|
+
expected.append(type(None))
|
|
559
|
+
return SchemaIssues(
|
|
560
|
+
[
|
|
561
|
+
SchemaIssue(
|
|
562
|
+
path=[],
|
|
563
|
+
message=f"{type(val).__name__} is not a list of strings!",
|
|
564
|
+
expected=expected,
|
|
565
|
+
found=type(val),
|
|
566
|
+
)
|
|
567
|
+
]
|
|
568
|
+
)
|
|
521
569
|
elif schema.type in ["bool", "str", "int", "float"]:
|
|
522
570
|
type_to_check = getattr(builtins, schema.type)
|
|
571
|
+
if not isinstance(val, type_to_check):
|
|
572
|
+
expected = [type_to_check]
|
|
573
|
+
if schema.optional:
|
|
574
|
+
expected.append(type(None))
|
|
575
|
+
return SchemaIssues(
|
|
576
|
+
[
|
|
577
|
+
SchemaIssue(
|
|
578
|
+
path=[],
|
|
579
|
+
message=f"{type(val).__name__} is not a {schema.type}!",
|
|
580
|
+
expected=expected,
|
|
581
|
+
found=type(val),
|
|
582
|
+
)
|
|
583
|
+
]
|
|
584
|
+
)
|
|
523
585
|
else:
|
|
524
586
|
raise ValueError(f"Invalid typ_name in schema: {schema.type}")
|
|
525
587
|
|
|
526
|
-
# Otherwise straight type check using typeguard
|
|
527
|
-
try:
|
|
528
|
-
check_type(val, type_to_check)
|
|
529
|
-
except TypeCheckError as t:
|
|
530
|
-
expected = [type_to_check]
|
|
531
|
-
if schema.optional:
|
|
532
|
-
expected.append(type(None))
|
|
533
|
-
return SchemaIssues(
|
|
534
|
-
[SchemaIssue(path=[], message=str(t), expected=expected, found=type(val))]
|
|
535
|
-
)
|
|
536
|
-
|
|
537
588
|
# List of literals given?
|
|
538
589
|
if schema.literal is not None:
|
|
539
590
|
for lit in schema.literal:
|
xradio/schema/common.py
CHANGED
|
File without changes
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
import xarray as xr
|
|
3
|
+
|
|
4
|
+
from xradio.measurement_set import load_processing_set
|
|
5
|
+
from xradio.schema.check import check_datatree
|
|
6
|
+
|
|
7
|
+
from xradio.measurement_set.measurement_set_xdt import (
|
|
8
|
+
MeasurementSetXdt,
|
|
9
|
+
InvalidAccessorLocation,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_simple():
|
|
14
|
+
assert True
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_simple_string(sample_fixture):
|
|
18
|
+
print(f"Sample fixture value: {sample_fixture}")
|
|
19
|
+
assert sample_fixture == "sample_data"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_sel():
|
|
23
|
+
ms_xdt = MeasurementSetXdt(xr.DataTree())
|
|
24
|
+
|
|
25
|
+
with pytest.raises(InvalidAccessorLocation, match="not a MSv4 node"):
|
|
26
|
+
assert ms_xdt.sel()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class TestLoadProcessingSet:
|
|
30
|
+
"""Tests for load_processing_set using real data"""
|
|
31
|
+
|
|
32
|
+
# This is a simple test to ensure that pytest is working correctly.
|
|
33
|
+
# It should be run with pytest to verify that the testing framework is set up properly.
|
|
34
|
+
def test_pytest_setup(self):
|
|
35
|
+
"""A simple test to ensure pytest is set up correctly."""
|
|
36
|
+
assert True, "Pytest setup is working correctly."
|
|
37
|
+
|
|
38
|
+
@pytest.mark.parametrize(
|
|
39
|
+
"convert_measurement_set_to_processing_set",
|
|
40
|
+
["Antennae_North.cal.lsrk.split.ms"],
|
|
41
|
+
indirect=True,
|
|
42
|
+
)
|
|
43
|
+
def test_check_datatree(self, convert_measurement_set_to_processing_set):
|
|
44
|
+
"""Test that the converted MS to PS complies with the datatree schema checker"""
|
|
45
|
+
ps_xdt = load_processing_set(str(convert_measurement_set_to_processing_set))
|
|
46
|
+
issues = check_datatree(ps_xdt)
|
|
47
|
+
# The check_datatree function returns a SchemaIssues object, not a string
|
|
48
|
+
assert (
|
|
49
|
+
str(issues) == "No schema issues found"
|
|
50
|
+
), f"Schema validation failed: {issues}"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
if __name__ == "__main__":
|
|
54
|
+
pytest.main(["-v", "-s", __file__])
|
|
55
|
+
# This is a simple test to ensure that pytest is working correctly.
|
|
56
|
+
# It should be run with pytest to verify that the testing framework is set up properly.
|
|
57
|
+
# To run this test, use the command: pytest -v __test_example__.py
|
|
58
|
+
# This will execute the test and show the results in verbose mode.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Test utilities for xradio - usable in both pytest and ASV benchmarks.
|
|
3
|
+
|
|
4
|
+
This module provides reusable test utilities for generating test data,
|
|
5
|
+
custom assertions, and helper functions for testing xradio functionality.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import toolviper.utils.logger as _logger
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
# Generators
|
|
12
|
+
"gen_test_ms",
|
|
13
|
+
"make_ms_empty",
|
|
14
|
+
"gen_minimal_ms",
|
|
15
|
+
# Validators
|
|
16
|
+
"check_msv4_matches_descr",
|
|
17
|
+
"check_processing_set_matches_msv2_descr",
|
|
18
|
+
# IO helpers
|
|
19
|
+
"download_measurement_set",
|
|
20
|
+
"build_processing_set_from_msv2",
|
|
21
|
+
"build_msv4_partition",
|
|
22
|
+
"build_minimal_msv4_xdt",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
# Export validators
|
|
26
|
+
from xradio.testing.measurement_set.checker import (
|
|
27
|
+
check_msv4_matches_descr,
|
|
28
|
+
check_processing_set_matches_msv2_descr,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# Export IO helpers (non-casacore dependent)
|
|
32
|
+
from xradio.testing.measurement_set.io import (
|
|
33
|
+
download_measurement_set,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# Casacore-dependent imports (with fallback)
|
|
37
|
+
try:
|
|
38
|
+
# Export generators
|
|
39
|
+
from xradio.testing.measurement_set.msv2_io import (
|
|
40
|
+
gen_test_ms,
|
|
41
|
+
make_ms_empty,
|
|
42
|
+
gen_minimal_ms,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# Export IO helpers (casacore-dependent)
|
|
46
|
+
from xradio.testing.measurement_set.msv2_io import (
|
|
47
|
+
build_processing_set_from_msv2,
|
|
48
|
+
build_msv4_partition,
|
|
49
|
+
build_minimal_msv4_xdt,
|
|
50
|
+
)
|
|
51
|
+
except ModuleNotFoundError as exc:
|
|
52
|
+
_logger.warning(
|
|
53
|
+
"Could not import casacore tables to handle MSv2. "
|
|
54
|
+
"Could not import the functions to convert from MSv2 to MSv4. "
|
|
55
|
+
f"That functionality will not be available. Details: {exc}"
|
|
56
|
+
)
|
|
57
|
+
else:
|
|
58
|
+
__all__.extend(["convert_msv2_to_processing_set"])
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Checks that an MSv4 converted from a generated MSv2 (gen_test_ms) has:
|
|
3
|
+
- the expected sizes
|
|
4
|
+
- input IDs (for antennas, fields, scans, etc.)
|
|
5
|
+
- data column
|
|
6
|
+
- etc. structure
|
|
7
|
+
of the generated MSv2 given as input to the MSv2=>MSv4 converter.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
import xarray as xr
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def check_msv4_matches_descr(msv4_xdt, msv2_descr):
|
|
15
|
+
"""
|
|
16
|
+
Checks a single partition / MSv4
|
|
17
|
+
|
|
18
|
+
Parameters
|
|
19
|
+
----------
|
|
20
|
+
msv4_xdt : xr.DataTree
|
|
21
|
+
An MSv4 DataTree containing one MSv4 partition
|
|
22
|
+
|
|
23
|
+
msv2_descr : dict
|
|
24
|
+
MSv2 description that was used to generate a test input MSv2 in gen_test_ms
|
|
25
|
+
|
|
26
|
+
"""
|
|
27
|
+
msv2_to_msv4_data_colname = {
|
|
28
|
+
"DATA": "VISIBILITY",
|
|
29
|
+
"MODEL_DATA": "VISIBILITY_MODEL",
|
|
30
|
+
"CORRECTED_DATA": "VISIBILITY_CORRECTED",
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
msv4_xds = msv4_xdt.ds
|
|
34
|
+
assert (msv2_descr["nrows_per_ddi"],) == msv4_xds.time.shape
|
|
35
|
+
nantennas = len(msv2_descr["ANTENNA"])
|
|
36
|
+
nbaselines = int(nantennas * (nantennas - 1) / 2)
|
|
37
|
+
assert (nbaselines,) == msv4_xds.baseline_id.shape
|
|
38
|
+
assert (msv2_descr["nchans"],) == msv4_xds.frequency.shape
|
|
39
|
+
assert (msv2_descr["nchans"],) == msv4_xds.frequency.shape
|
|
40
|
+
assert (msv2_descr["npols"],) == msv4_xds.polarization.shape
|
|
41
|
+
|
|
42
|
+
assert "scan_name" in msv4_xds
|
|
43
|
+
assert len(np.unique(msv4_xds.coords["scan_name"])) == 1
|
|
44
|
+
|
|
45
|
+
for msv2_data_colname in msv2_descr["data_cols"]:
|
|
46
|
+
msv4_data_colname = msv2_to_msv4_data_colname[msv2_data_colname]
|
|
47
|
+
assert msv4_data_colname in msv4_xds.data_vars
|
|
48
|
+
|
|
49
|
+
data_shape = msv4_xds.data_vars[msv4_data_colname].shape
|
|
50
|
+
assert msv2_descr["nrows_per_ddi"] == data_shape[0]
|
|
51
|
+
assert nbaselines == data_shape[1]
|
|
52
|
+
assert msv2_descr["nchans"] == data_shape[2]
|
|
53
|
+
assert msv2_descr["npols"] == data_shape[3]
|
|
54
|
+
|
|
55
|
+
ant_xds = msv4_xdt["antenna_xds"].ds
|
|
56
|
+
assert "antenna_name" in ant_xds
|
|
57
|
+
assert len(ant_xds.coords["antenna_name"]) == nantennas
|
|
58
|
+
|
|
59
|
+
if msv2_descr["params"]["misbehave"]:
|
|
60
|
+
expected_type = "field_and_source"
|
|
61
|
+
else:
|
|
62
|
+
expected_type = "field_and_source_ephemeris"
|
|
63
|
+
|
|
64
|
+
field_and_source_name = "field_and_source_base_xds"
|
|
65
|
+
assert (
|
|
66
|
+
field_and_source_name
|
|
67
|
+
== msv4_xdt.ds.attrs["data_groups"]["base"]["field_and_source"]
|
|
68
|
+
)
|
|
69
|
+
assert field_and_source_name in msv4_xdt
|
|
70
|
+
assert msv4_xdt[field_and_source_name].ds.attrs["type"] == expected_type
|
|
71
|
+
|
|
72
|
+
if msv2_descr["params"]["opt_tables"]:
|
|
73
|
+
assert "system_calibration_xds" in msv4_xdt
|
|
74
|
+
assert "weather_xds" in msv4_xdt
|
|
75
|
+
if not msv2_descr["params"]["misbehave"]:
|
|
76
|
+
assert "SOURCE_DIRECTION" in msv4_xdt["field_and_source_base_xds"].ds
|
|
77
|
+
assert (
|
|
78
|
+
msv4_xdt["field_and_source_base_xds"].attrs["type"]
|
|
79
|
+
== "field_and_source_ephemeris"
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
if msv2_descr["params"]["vlbi_tables"]:
|
|
83
|
+
assert "gain_curve_xds" in msv4_xdt
|
|
84
|
+
assert "phase_calibration_xds" in msv4_xdt
|
|
85
|
+
|
|
86
|
+
partition_info = msv4_xdt.xr_ms.get_partition_info()
|
|
87
|
+
processor_info = msv4_xdt.ds.attrs["processor_info"]
|
|
88
|
+
if msv2_descr["params"]["misbehave"]:
|
|
89
|
+
# SPW names should be empty string in MSv2
|
|
90
|
+
assert partition_info["spectral_window_name"] == "spw_0"
|
|
91
|
+
assert not processor_info["type"]
|
|
92
|
+
assert not processor_info["sub_type"]
|
|
93
|
+
else:
|
|
94
|
+
assert partition_info["spectral_window_name"]
|
|
95
|
+
assert processor_info["type"]
|
|
96
|
+
assert processor_info["sub_type"]
|
|
97
|
+
|
|
98
|
+
observation_info = msv4_xdt.ds.attrs["observation_info"]
|
|
99
|
+
if msv2_descr["params"]["opt_tables"] and not msv2_descr["params"]["misbehave"]:
|
|
100
|
+
assert "session_reference_UID" in observation_info
|
|
101
|
+
else:
|
|
102
|
+
assert "session_reference_UID" not in observation_info
|
|
103
|
+
|
|
104
|
+
if (
|
|
105
|
+
(msv2_descr["params"]["opt_tables"] and not msv2_descr["params"]["misbehave"])
|
|
106
|
+
) or "OBSERVATION" in msv2_descr:
|
|
107
|
+
assert "execution_block_UID" in observation_info
|
|
108
|
+
assert "scheduling_block_UID" in observation_info
|
|
109
|
+
else:
|
|
110
|
+
assert "execution_block_UID" not in observation_info
|
|
111
|
+
assert "scheduling_block_UID" not in observation_info
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def check_processing_set_matches_msv2_descr(
|
|
115
|
+
proc_set_xdt: xr.DataTree, msv2_descr: dict
|
|
116
|
+
):
|
|
117
|
+
"""
|
|
118
|
+
Parameters
|
|
119
|
+
----------
|
|
120
|
+
proc_set_xdt : xr.DataTree
|
|
121
|
+
A processing set DataTree containing MSv4s to be checked
|
|
122
|
+
|
|
123
|
+
msv2_descr : dict
|
|
124
|
+
MSv2 description that was used to generate a test input MSv2 in gen_test_ms
|
|
125
|
+
|
|
126
|
+
Returns
|
|
127
|
+
-------
|
|
128
|
+
|
|
129
|
+
"""
|
|
130
|
+
for _msv4_name, msv4_xdt in proc_set_xdt.items():
|
|
131
|
+
check_msv4_matches_descr(msv4_xdt, msv2_descr)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""
|
|
2
|
+
I/O helpers to download reusable MeasurementSets.
|
|
3
|
+
|
|
4
|
+
Casacore-dependent functions have been moved to msv2_io.py.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from toolviper.utils.data import download
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def download_measurement_set(input_ms: str, directory: str | Path = "/tmp") -> Path:
|
|
15
|
+
"""
|
|
16
|
+
Download a MeasurementSet v2 archive into the given directory.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
directory = Path(directory)
|
|
20
|
+
directory.mkdir(parents=True, exist_ok=True)
|
|
21
|
+
download(file=input_ms, folder=str(directory))
|
|
22
|
+
return directory / input_ms
|