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.
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 +1 -1
  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.2.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.2.dist-info → xradio-1.1.0.dist-info}/WHEEL +1 -1
  42. xradio-1.0.2.dist-info/RECORD +0 -66
  43. {xradio-1.0.2.dist-info → xradio-1.1.0.dist-info}/licenses/LICENSE.txt +0 -0
  44. {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 not isinstance(val, xarray.DataArray):
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
- # Fall through to plain type check
515
- type_to_check = dict
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
- type_to_check = typing.List[str]
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
@@ -9,7 +9,6 @@ from xradio.schema.bases import (
9
9
  from xradio.schema.typing import Attr, Coord, Coordof, Data, Dataof, Name
10
10
  import numpy
11
11
 
12
-
13
12
  # Dimensions
14
13
  Time = Literal["time"]
15
14
  """ Observation time dimension """
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