jolly-roger 0.0.2__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.
Potentially problematic release.
This version of jolly-roger might be problematic. Click here for more details.
- jolly_roger/__init__.py +11 -0
- jolly_roger/_version.py +21 -0
- jolly_roger/_version.pyi +4 -0
- jolly_roger/baselines.py +156 -0
- jolly_roger/flagger.py +101 -0
- jolly_roger/hour_angles.py +159 -0
- jolly_roger/logging.py +48 -0
- jolly_roger/py.typed +0 -0
- jolly_roger/uvws.py +289 -0
- jolly_roger-0.0.2.dist-info/METADATA +87 -0
- jolly_roger-0.0.2.dist-info/RECORD +14 -0
- jolly_roger-0.0.2.dist-info/WHEEL +4 -0
- jolly_roger-0.0.2.dist-info/entry_points.txt +2 -0
- jolly_roger-0.0.2.dist-info/licenses/LICENSE +29 -0
jolly_roger/__init__.py
ADDED
jolly_roger/_version.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# file generated by setuptools-scm
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
|
|
4
|
+
__all__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
|
|
5
|
+
|
|
6
|
+
TYPE_CHECKING = False
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from typing import Tuple
|
|
9
|
+
from typing import Union
|
|
10
|
+
|
|
11
|
+
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
12
|
+
else:
|
|
13
|
+
VERSION_TUPLE = object
|
|
14
|
+
|
|
15
|
+
version: str
|
|
16
|
+
__version__: str
|
|
17
|
+
__version_tuple__: VERSION_TUPLE
|
|
18
|
+
version_tuple: VERSION_TUPLE
|
|
19
|
+
|
|
20
|
+
__version__ = version = '0.0.2'
|
|
21
|
+
__version_tuple__ = version_tuple = (0, 0, 2)
|
jolly_roger/_version.pyi
ADDED
jolly_roger/baselines.py
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""Routines and structures to describe antennas, their
|
|
2
|
+
XYZ and baseline vectors"""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from argparse import ArgumentParser
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from itertools import combinations
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import astropy.units as u
|
|
12
|
+
import matplotlib.pyplot as plt
|
|
13
|
+
import numpy as np
|
|
14
|
+
from casacore.tables import table
|
|
15
|
+
|
|
16
|
+
from jolly_roger.logging import logger
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class Baselines:
|
|
21
|
+
"""Container representing the antennas found in some measurement set, their
|
|
22
|
+
baselines and associated mappings. Only the upper triangle of
|
|
23
|
+
baselines are formed, e.g. 1-2 not 2-1.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
ant_xyz: np.ndarray
|
|
27
|
+
"""Antenna (X,Y,Z) coordinates taken from the measurement set"""
|
|
28
|
+
b_xyz: np.ndarray
|
|
29
|
+
"""The baseline vectors formed from each antenna-pair"""
|
|
30
|
+
b_idx: np.ndarray
|
|
31
|
+
"""Baselihe indices representing a pair of antenna"""
|
|
32
|
+
b_map: dict[tuple[int, int], int]
|
|
33
|
+
"""A mapping between two antennas to their baseline index"""
|
|
34
|
+
ms_path: Path
|
|
35
|
+
"""The measurement set used to construct some instance of `Baseline`"""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def get_baselines_from_ms(ms_path: Path) -> Baselines:
|
|
39
|
+
"""Extract the antenna positions from the nominated measurement
|
|
40
|
+
set and constructed the set of baselines. These are drawn from
|
|
41
|
+
the ANTENNA table in the measurement set.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
ms_path (Path): The measurement set to extract baseliens from
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
Baselines: The corresponding set of baselines formed.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
logger.info(f"Creating baseline instance from {ms_path=}")
|
|
51
|
+
with table(str(ms_path / "ANTENNA"), ack=False) as tab:
|
|
52
|
+
ants_idx = np.arange(len(tab), dtype=int)
|
|
53
|
+
b_idx = np.array(list(combinations(list(ants_idx), 2)))
|
|
54
|
+
xyz = tab.getcol("POSITION")
|
|
55
|
+
b_xyz = xyz[b_idx[:, 0]] - xyz[b_idx[:, 1]]
|
|
56
|
+
|
|
57
|
+
b_map = {tuple(k): idx for idx, k in enumerate(b_idx)}
|
|
58
|
+
|
|
59
|
+
logger.info(f"ants={len(ants_idx)}, baselines={b_idx.shape[0]}")
|
|
60
|
+
return Baselines(
|
|
61
|
+
ant_xyz=xyz * u.m, b_xyz=b_xyz * u.m, b_idx=b_idx, b_map=b_map, ms_path=ms_path
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass
|
|
66
|
+
class BaselinePlotPaths:
|
|
67
|
+
"""Names for plots for baseline visualisations"""
|
|
68
|
+
|
|
69
|
+
antenna_path: Path
|
|
70
|
+
"""Output for the antenna XYZ plot"""
|
|
71
|
+
baseline_path: Path
|
|
72
|
+
"""Output for the baselines vector plot"""
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def make_plot_names(ms_path: Path) -> BaselinePlotPaths:
|
|
76
|
+
"""Construct the output paths of the diagnostic plots
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
ms_path (Path): The measurement set the plots are created for
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
BaselinePlotPaths: The output paths for the plot names
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
basename = ms_path.parent / ms_path.stem
|
|
86
|
+
|
|
87
|
+
antenna_path = Path(f"{basename!s}-antenna.pdf")
|
|
88
|
+
baseline_path = Path(f"{basename!s}-baseline.pdf")
|
|
89
|
+
|
|
90
|
+
return BaselinePlotPaths(antenna_path=antenna_path, baseline_path=baseline_path)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def plot_baselines(baselines: Baselines) -> BaselinePlotPaths:
|
|
94
|
+
"""Create basic diagnostic plots for a set of baselines. This
|
|
95
|
+
includes the antenna positions and the baseline vectors.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
baselines (Baselines): The loaded instance of the baselines from a measurement set
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
BaselinePlotPaths: The output paths of the plots created
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
plot_names = make_plot_names(ms_path=baselines.ms_path)
|
|
105
|
+
|
|
106
|
+
# Make the initial antenna plot
|
|
107
|
+
fig, ax = plt.subplots(1, 1)
|
|
108
|
+
|
|
109
|
+
ax.scatter(baselines.b_xyz[:, 0], baselines.b_xyz[:, 1], label="Baseline")
|
|
110
|
+
|
|
111
|
+
ax.set(xlabel="X (meters)", ylabel="Y (meters)", title="ASKAP Baseline Vectors")
|
|
112
|
+
ax.legend()
|
|
113
|
+
|
|
114
|
+
fig.tight_layout()
|
|
115
|
+
fig.savefig(plot_names.baseline_path)
|
|
116
|
+
|
|
117
|
+
# Now plot the antennas
|
|
118
|
+
fig, ax = plt.subplots(1, 1)
|
|
119
|
+
|
|
120
|
+
ax.scatter(baselines.ant_xyz[:, 0], baselines.ant_xyz[:, 1], label="Antenna")
|
|
121
|
+
|
|
122
|
+
ax.set(xlabel="X (meters)", ylabel="Y (meters)", title="ASKAP Antenna positions")
|
|
123
|
+
ax.legend()
|
|
124
|
+
fig.tight_layout()
|
|
125
|
+
fig.savefig(plot_names.antenna_path)
|
|
126
|
+
|
|
127
|
+
return plot_names
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def get_parser() -> ArgumentParser:
|
|
131
|
+
parser = ArgumentParser(
|
|
132
|
+
description="Extract and plot antenna dna baseline information from a measurement set"
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
sub_parsers = parser.add_subparsers(dest="mode")
|
|
136
|
+
|
|
137
|
+
plot_parser = sub_parsers.add_parser(
|
|
138
|
+
"plot", description="Basic plots around baselines"
|
|
139
|
+
)
|
|
140
|
+
plot_parser.add_argument("ms_path", type=Path, help="Path to the measurement set")
|
|
141
|
+
|
|
142
|
+
return parser
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def cli() -> None:
|
|
146
|
+
parser: ArgumentParser = get_parser()
|
|
147
|
+
|
|
148
|
+
args = parser.parse_args()
|
|
149
|
+
|
|
150
|
+
if args.mode == "plot":
|
|
151
|
+
baselines = get_baselines_from_ms(ms_path=args.ms_path)
|
|
152
|
+
plot_baselines(baselines=baselines)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
if __name__ == "__main__":
|
|
156
|
+
cli()
|
jolly_roger/flagger.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Flagging utility for a MS"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from argparse import ArgumentParser
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import astropy.units as u
|
|
10
|
+
|
|
11
|
+
from jolly_roger.baselines import get_baselines_from_ms
|
|
12
|
+
from jolly_roger.hour_angles import make_hour_angles_for_ms
|
|
13
|
+
from jolly_roger.logging import logger
|
|
14
|
+
from jolly_roger.uvws import uvw_flagger, xyz_to_uvw
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class JollyRogerFlagOptions:
|
|
19
|
+
"""Specifications of the flagging to carry out"""
|
|
20
|
+
|
|
21
|
+
min_scale_deg: float = 0.075
|
|
22
|
+
"""Minimum angular scale to project to UVW"""
|
|
23
|
+
min_horizon_limit_deg: float = -3
|
|
24
|
+
"""The minimum elevation for the sun projected baselines to be considered for flagging"""
|
|
25
|
+
max_horizon_limit_deg: float = 90
|
|
26
|
+
"""The minimum elevation for the sun projected baselines to be considered for flagging"""
|
|
27
|
+
dry_run: bool = False
|
|
28
|
+
"""Do not apply the flags"""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def flag(ms_path: Path, flag_options: JollyRogerFlagOptions) -> Path:
|
|
32
|
+
# Trust no one
|
|
33
|
+
logger.debug(f"{flag_options=}")
|
|
34
|
+
|
|
35
|
+
ms_path = Path(ms_path)
|
|
36
|
+
logger.info(f"Flagging {ms_path=}")
|
|
37
|
+
|
|
38
|
+
baselines = get_baselines_from_ms(ms_path=ms_path)
|
|
39
|
+
hour_angles = make_hour_angles_for_ms(ms_path=ms_path, position="sun")
|
|
40
|
+
|
|
41
|
+
uvws = xyz_to_uvw(baselines=baselines, hour_angles=hour_angles)
|
|
42
|
+
ms_path = uvw_flagger(
|
|
43
|
+
computed_uvws=uvws,
|
|
44
|
+
min_horizon_lim=flag_options.min_horizon_limit_deg * u.deg,
|
|
45
|
+
max_horizon_lim=flag_options.max_horizon_limit_deg * u.deg,
|
|
46
|
+
min_sun_scale=flag_options.min_scale_deg * u.deg,
|
|
47
|
+
dry_run=flag_options.dry_run,
|
|
48
|
+
)
|
|
49
|
+
logger.info(f"Finished processing {ms_path=}")
|
|
50
|
+
|
|
51
|
+
return ms_path
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def get_parser() -> ArgumentParser:
|
|
55
|
+
parser = ArgumentParser(
|
|
56
|
+
description="Flag a measurement set based on properties of the Sun"
|
|
57
|
+
)
|
|
58
|
+
parser.add_argument("ms_path", type=Path, help="The measurement set to flag")
|
|
59
|
+
|
|
60
|
+
parser.add_argument(
|
|
61
|
+
"--min-scale-deg",
|
|
62
|
+
type=float,
|
|
63
|
+
default=0.075,
|
|
64
|
+
help="The minimum scale required for flagging",
|
|
65
|
+
)
|
|
66
|
+
parser.add_argument(
|
|
67
|
+
"--min-horizon-limit-deg",
|
|
68
|
+
type=float,
|
|
69
|
+
default=-3,
|
|
70
|
+
help="The minimum elevation of the centroid of the object (e.g. sun) for uvw flagging to be activated",
|
|
71
|
+
)
|
|
72
|
+
parser.add_argument(
|
|
73
|
+
"--max-horizon-limit-deg",
|
|
74
|
+
type=float,
|
|
75
|
+
default=-3,
|
|
76
|
+
help="The maximum elevation of the centroid of the object (e.g. sun) for uvw flagging to be activated",
|
|
77
|
+
)
|
|
78
|
+
parser.add_argument(
|
|
79
|
+
"--dry-run", action="store_true", help="Do not apply the computed flags"
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
return parser
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def cli() -> None:
|
|
86
|
+
parser = get_parser()
|
|
87
|
+
|
|
88
|
+
args = parser.parse_args()
|
|
89
|
+
|
|
90
|
+
flag_options = JollyRogerFlagOptions(
|
|
91
|
+
min_scale_deg=args.min_scale_deg,
|
|
92
|
+
min_horizon_limit_deg=args.min_horizon_limit_deg,
|
|
93
|
+
max_horizon_limit_deg=args.max_horizon_limit_deg,
|
|
94
|
+
dry_run=args.dry_run,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
flag(ms_path=args.ms_path, flag_options=flag_options)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
if __name__ == "__main__":
|
|
101
|
+
cli()
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""Utilities to construct properties that scale over hour angle"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Literal
|
|
8
|
+
|
|
9
|
+
import astropy.units as u
|
|
10
|
+
import numpy as np
|
|
11
|
+
from astropy.coordinates import EarthLocation, SkyCoord, get_sun
|
|
12
|
+
from astropy.time import Time
|
|
13
|
+
from casacore.tables import table
|
|
14
|
+
|
|
15
|
+
from jolly_roger.logging import logger
|
|
16
|
+
|
|
17
|
+
# Default location with XYZ based on mean of antenna positions
|
|
18
|
+
ASKAP_XYZ_m = np.array([-2556146.66356375, 5097426.58592797, -2848333.08164107]) * u.m
|
|
19
|
+
ASKAP = EarthLocation(*ASKAP_XYZ_m)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class PositionHourAngles:
|
|
24
|
+
"""Represent time, hour angles and other quantities for some
|
|
25
|
+
assumed sky position. Time intervals are intended to represent
|
|
26
|
+
those stored in a measurement set."""
|
|
27
|
+
|
|
28
|
+
hour_angle: u.rad
|
|
29
|
+
"""The hour angle across sampled time intervales of a source for a Earth location"""
|
|
30
|
+
time_mjds: np.ndarray
|
|
31
|
+
"""The MJD time in seconds from which other quantities are evalauted against. Should be drawn from a measurement set."""
|
|
32
|
+
location: EarthLocation
|
|
33
|
+
"""The location these quantities have been derived from."""
|
|
34
|
+
position: SkyCoord
|
|
35
|
+
"""The sky-position that is being used to calculate quantities towards"""
|
|
36
|
+
elevation: np.ndarray
|
|
37
|
+
"""The elevation of the ``position` direction across time"""
|
|
38
|
+
time: Time
|
|
39
|
+
"""Representation of the `time_mjds` attribute"""
|
|
40
|
+
time_map: dict[float, int]
|
|
41
|
+
"""Index mapping of time steps described in `time_mjds` to an array index position.
|
|
42
|
+
This is done by selecting all unique `time_mjds` and ordering in this 'first seen'
|
|
43
|
+
position"""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _process_position(
|
|
47
|
+
position: SkyCoord | Literal["sun"] | None = None,
|
|
48
|
+
ms_path: Path | None = None,
|
|
49
|
+
times: Time | None = None,
|
|
50
|
+
) -> SkyCoord:
|
|
51
|
+
"""Acquire a SkyCoord object towards a specified position. If
|
|
52
|
+
a known string position is provided this will be looked up and
|
|
53
|
+
may required the `times` (e.g. for the sun). Otherwise is position
|
|
54
|
+
is None it will be drawn from the PHASE_DIR in the provided measurement
|
|
55
|
+
set
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
position (SkyCoord | Literal["sun"] | None, optional): The position to be considered. Defaults to None.
|
|
59
|
+
ms_path (Path | None, optional): The path with the PHASE_DIR to use should `position` be None. Defaults to None.
|
|
60
|
+
times (Time | None, optional): Times to used if they are required in the lookup. Defaults to None.
|
|
61
|
+
|
|
62
|
+
Raises:
|
|
63
|
+
ValueError: Raised if a string position is provided without a `times`
|
|
64
|
+
ValueError: Raised is position is None and no ms_path provided
|
|
65
|
+
ValueError: Raised if no final SkyCoord is constructed
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
SkyCoord: The position to use
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
if isinstance(position, str):
|
|
72
|
+
if times is None:
|
|
73
|
+
msg = f"{times=}, but needs to be set when position is a name"
|
|
74
|
+
raise ValueError(msg)
|
|
75
|
+
if position == "sun":
|
|
76
|
+
logger.info("Getting sky-position of the sun")
|
|
77
|
+
position = get_sun(times)
|
|
78
|
+
|
|
79
|
+
if position is None:
|
|
80
|
+
if ms_path is None:
|
|
81
|
+
msg = f"{position=}, so default position can't be drawn. Provide a ms_path="
|
|
82
|
+
raise ValueError(msg)
|
|
83
|
+
|
|
84
|
+
with table(str(ms_path / "FIELD")) as tab:
|
|
85
|
+
logger.info(f"Getting the sky-position from PHASE_DIR of {ms_path=}")
|
|
86
|
+
field_positions = tab.getcol("PHASE_DIR")
|
|
87
|
+
position = SkyCoord(field_positions[0] * u.rad)
|
|
88
|
+
|
|
89
|
+
if isinstance(position, SkyCoord):
|
|
90
|
+
return position
|
|
91
|
+
|
|
92
|
+
# Someone sea dog is having a laugh
|
|
93
|
+
msg = "Something went wrong in the processing of position"
|
|
94
|
+
raise ValueError(msg)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def make_hour_angles_for_ms(
|
|
98
|
+
ms_path: Path,
|
|
99
|
+
location: EarthLocation = ASKAP,
|
|
100
|
+
position: SkyCoord | str | None = None,
|
|
101
|
+
whole_day: bool = False,
|
|
102
|
+
) -> PositionHourAngles:
|
|
103
|
+
"""Calculate hour-angle and time quantities for a given position using time information
|
|
104
|
+
encoded in a nominated measurement set at a nominated location
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
ms_path (Path): Measurement set to usefor time and sky-position information
|
|
108
|
+
location (EarthLocation, optional): The location to use when calculate LST. Defaults to ASKAP.
|
|
109
|
+
position (SkyCoord | str | None, optional): The sky-direction hour-angles will be calculated towards. Defaults to None.
|
|
110
|
+
whole_day (bool, optional): Calaculate for a 24 hour persion starting from the first time step. Defaults to False.
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
PositionHourAngle: Compute hour angles, normalised times and elevation
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
logger.info(f"Computing hour angles for {ms_path=}")
|
|
117
|
+
with table(str(ms_path), ack=False) as tab:
|
|
118
|
+
logger.info("Extracting timesteps and constructing time mapping")
|
|
119
|
+
times_mjds = tab.getcol("TIME_CENTROID")
|
|
120
|
+
|
|
121
|
+
# get unique time steps and make sure they are in their first appeared order
|
|
122
|
+
times_mjds, indices = np.unique(times_mjds, return_index=True)
|
|
123
|
+
sorted_idx = np.argsort(indices)
|
|
124
|
+
times_mjds = times_mjds[sorted_idx]
|
|
125
|
+
time_map = {k: idx for idx, k in enumerate(times_mjds)}
|
|
126
|
+
|
|
127
|
+
if whole_day:
|
|
128
|
+
logger.info(f"Assuming a full day from {times_mjds} MJD (seconds)")
|
|
129
|
+
time_step = times_mjds[1] - times_mjds[0]
|
|
130
|
+
times_mjds = times_mjds[0] + time_step * np.arange(
|
|
131
|
+
int(60 * 60 * 24 / time_step)
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
times = Time(times_mjds / 60 / 60 / 24, format="mjd")
|
|
135
|
+
|
|
136
|
+
sky_position: SkyCoord = _process_position(
|
|
137
|
+
position=position, times=times, ms_path=ms_path
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
lst = times.sidereal_time("apparent", longitude=location.lon)
|
|
141
|
+
hour_angle = lst - sky_position.ra
|
|
142
|
+
mask = hour_angle > 12 * u.hourangle
|
|
143
|
+
hour_angle[mask] -= 24 * u.hourangle
|
|
144
|
+
|
|
145
|
+
logger.info("Creatring elevation curve")
|
|
146
|
+
sin_alt = np.arcsin(
|
|
147
|
+
np.sin(location.lat) * np.sin(sky_position[0].dec.rad)
|
|
148
|
+
+ np.cos(location.lat) * np.cos(sky_position.dec.rad) * np.cos(hour_angle)
|
|
149
|
+
).to(u.rad)
|
|
150
|
+
|
|
151
|
+
return PositionHourAngles(
|
|
152
|
+
hour_angle=hour_angle,
|
|
153
|
+
time_mjds=times_mjds,
|
|
154
|
+
location=location,
|
|
155
|
+
position=sky_position,
|
|
156
|
+
elevation=sin_alt,
|
|
157
|
+
time=times,
|
|
158
|
+
time_map=time_map,
|
|
159
|
+
)
|
jolly_roger/logging.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Basic python logging setup"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Any, ClassVar
|
|
7
|
+
|
|
8
|
+
# Create logger
|
|
9
|
+
logging.captureWarnings(True)
|
|
10
|
+
logger = logging.getLogger("jolly_rodger")
|
|
11
|
+
logger.setLevel(logging.INFO)
|
|
12
|
+
|
|
13
|
+
# Create console handler and set level to debug
|
|
14
|
+
ch = logging.StreamHandler()
|
|
15
|
+
ch.setLevel(logging.DEBUG)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class CustomFormatter(logging.Formatter):
|
|
19
|
+
"""A custom logger formatter"""
|
|
20
|
+
|
|
21
|
+
grey = "\x1b[38;20m"
|
|
22
|
+
blue = "\x1b[34;20m"
|
|
23
|
+
green = "\x1b[32;20m"
|
|
24
|
+
yellow = "\x1b[33;20m"
|
|
25
|
+
red = "\x1b[31;20m"
|
|
26
|
+
bold_red = "\x1b[31;1m"
|
|
27
|
+
reset = "\x1b[0m"
|
|
28
|
+
format_str = "%(asctime)s.%(msecs)03d: %(message)s"
|
|
29
|
+
|
|
30
|
+
FORMATS: ClassVar = {
|
|
31
|
+
logging.DEBUG: f"{blue}%(levelname)s{reset} {format_str}",
|
|
32
|
+
logging.INFO: f"{green}%(levelname)s{reset} {format_str}",
|
|
33
|
+
logging.WARNING: f"{yellow}%(levelname)s{reset} {format_str}",
|
|
34
|
+
logging.ERROR: f"{red}%(levelname)s{reset} {format_str}",
|
|
35
|
+
logging.CRITICAL: f"{bold_red}%(levelname)s{reset} {format_str}",
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
def format(self, record: Any) -> Any:
|
|
39
|
+
log_fmt = self.FORMATS.get(record.levelno)
|
|
40
|
+
formatter = logging.Formatter(log_fmt, "%Y-%m-%d %H:%M:%S")
|
|
41
|
+
return formatter.format(record)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# Add formatter to ch
|
|
45
|
+
ch.setFormatter(CustomFormatter())
|
|
46
|
+
|
|
47
|
+
# Add ch to logger
|
|
48
|
+
logger.addHandler(ch)
|
jolly_roger/py.typed
ADDED
|
File without changes
|
jolly_roger/uvws.py
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
"""Calculating the UVWs for a measurement set towards a direction"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import astropy.units as u
|
|
9
|
+
import numpy as np
|
|
10
|
+
from astropy.constants import c as speed_of_light
|
|
11
|
+
from casacore.tables import table, taql
|
|
12
|
+
from tqdm import tqdm
|
|
13
|
+
|
|
14
|
+
from jolly_roger.baselines import Baselines
|
|
15
|
+
from jolly_roger.hour_angles import PositionHourAngles
|
|
16
|
+
from jolly_roger.logging import logger
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class UVWs:
|
|
21
|
+
"""A small container to represent uvws"""
|
|
22
|
+
|
|
23
|
+
uvws: np.ndarray
|
|
24
|
+
"""The (U,V,W) coordinates"""
|
|
25
|
+
hour_angles: PositionHourAngles
|
|
26
|
+
"""The hour angle information used to construct the UVWs"""
|
|
27
|
+
baselines: Baselines
|
|
28
|
+
"""The set of antenna baselines used for form the UVWs"""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def xyz_to_uvw(
|
|
32
|
+
baselines: Baselines,
|
|
33
|
+
hour_angles: PositionHourAngles,
|
|
34
|
+
) -> UVWs:
|
|
35
|
+
"""Generate the UVWs for a given set of baseline vectors towards a position
|
|
36
|
+
across a series of hour angles.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
baselines (Baselines): The set of baselines vectors to use
|
|
40
|
+
hour_angles (PositionHourAngles): The hour angles and position to generate UVWs for
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
UVWs: The generated set of UVWs
|
|
44
|
+
"""
|
|
45
|
+
b_xyz = baselines.b_xyz
|
|
46
|
+
|
|
47
|
+
# Getting the units right is important, mate
|
|
48
|
+
ha = hour_angles.hour_angle
|
|
49
|
+
ha = ha.to(u.rad)
|
|
50
|
+
|
|
51
|
+
declination = hour_angles.position.dec
|
|
52
|
+
declination = declination.to(u.rad)
|
|
53
|
+
|
|
54
|
+
# This is necessary for broadcastung in the matrix to work.
|
|
55
|
+
# Should the position be a solar object like the sub its position
|
|
56
|
+
# will change throughout the observation. but it will have
|
|
57
|
+
## been created consistently with the hour angles. If it is fixed
|
|
58
|
+
# then the use of the numpy ones like will ensure the same shape.
|
|
59
|
+
declination = (np.ones(len(ha)) * declination).decompose()
|
|
60
|
+
|
|
61
|
+
# Precompute the repeated terms
|
|
62
|
+
sin_ha = np.sin(ha)
|
|
63
|
+
sin_dec = np.sin(declination)
|
|
64
|
+
cos_ha = np.cos(ha)
|
|
65
|
+
cos_dec = np.cos(declination)
|
|
66
|
+
zeros = np.zeros_like(sin_ha)
|
|
67
|
+
|
|
68
|
+
# Conversion from baseline vectors to UVW
|
|
69
|
+
mat = np.array(
|
|
70
|
+
[
|
|
71
|
+
[sin_ha, cos_ha, zeros],
|
|
72
|
+
[-sin_dec * cos_ha, sin_dec * sin_ha, cos_dec],
|
|
73
|
+
[
|
|
74
|
+
np.cos(declination) * np.cos(ha),
|
|
75
|
+
np.cos(declination) * np.sin(ha),
|
|
76
|
+
np.sin(declination),
|
|
77
|
+
],
|
|
78
|
+
]
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# Every time this confuses me and I need the first mate to look over.
|
|
82
|
+
# b_xyz shape: (baselines, coord) where coord is XYZ
|
|
83
|
+
# mat shape: (3, 3, timesteps)
|
|
84
|
+
# uvw shape: (baseline, coord, timesteps) where coord is UVW
|
|
85
|
+
uvw = np.einsum("ijk,lj->lik", mat, b_xyz, optimize=True) # codespell:ignore lik
|
|
86
|
+
|
|
87
|
+
# Make order (coord, baseline, timesteps)
|
|
88
|
+
uvw = np.swapaxes(uvw, 0, 1)
|
|
89
|
+
logger.debug(f"{uvw.shape=}")
|
|
90
|
+
|
|
91
|
+
return UVWs(uvws=uvw, hour_angles=hour_angles, baselines=baselines)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@dataclass
|
|
95
|
+
class SunScale:
|
|
96
|
+
"""Describes the (u,v)-scales sensitive to angular scales of the Sun"""
|
|
97
|
+
|
|
98
|
+
min_scale_chan_lambda: u.Quantity
|
|
99
|
+
"""The distance that corresponds to an angular scale scaled to each channel set using the minimum angular scale"""
|
|
100
|
+
chan_lambda: u.Quantity
|
|
101
|
+
"""The wavelength of each channel"""
|
|
102
|
+
min_scale_deg: float
|
|
103
|
+
"""The minimum angular scale used for baseline flagging"""
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def get_sun_uv_scales(
|
|
107
|
+
ms_path: Path,
|
|
108
|
+
min_scale: u.Quantity = 0.075 * u.deg,
|
|
109
|
+
) -> SunScale:
|
|
110
|
+
"""Compute the angular scales and the corresponding (u,v)-distances that
|
|
111
|
+
would be sensitive to them.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
ms_path (Path): The measurement set to consider, where frequency information is extracted from
|
|
115
|
+
min_scale (u.Quantity, optional): The minimum angular scale that will be projected and flgged. Defaults to 0.075*u.deg.
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
SunScale: The sun scales in distances
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
with table(str(ms_path / "SPECTRAL_WINDOW")) as tab:
|
|
122
|
+
chan_freqs = tab.getcol("CHAN_FREQ")[0] * u.Hz
|
|
123
|
+
|
|
124
|
+
chan_lambda_m = np.squeeze((speed_of_light / chan_freqs).to(u.m))
|
|
125
|
+
|
|
126
|
+
sun_min_scale_chan_lambda = chan_lambda_m / min_scale.to(u.rad).value
|
|
127
|
+
|
|
128
|
+
return SunScale(
|
|
129
|
+
min_scale_chan_lambda=sun_min_scale_chan_lambda,
|
|
130
|
+
chan_lambda=chan_lambda_m,
|
|
131
|
+
min_scale_deg=min_scale,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@dataclass
|
|
136
|
+
class BaselineFlagSummary:
|
|
137
|
+
"""Container to capture the flagged baselines statistics"""
|
|
138
|
+
|
|
139
|
+
uvw_flag_perc: float
|
|
140
|
+
"""The percentage of flags to add based on the uv-distance cut"""
|
|
141
|
+
elevation_flag_perc: float
|
|
142
|
+
"""The percentage of flags to add based on the elevation cut"""
|
|
143
|
+
jolly_flag_perc: float
|
|
144
|
+
"""The percentage of new to add based on both criteria"""
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def log_summaries(
|
|
148
|
+
summary: dict[tuple[int, int], BaselineFlagSummary],
|
|
149
|
+
min_horizon_lim: u.Quantity,
|
|
150
|
+
max_horizon_lim: u.Quantity,
|
|
151
|
+
min_sun_scale: u.Quantity,
|
|
152
|
+
dry_run: bool = False,
|
|
153
|
+
) -> None:
|
|
154
|
+
"""Log the flagging statistics made throughout the `uvw_flagger`.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
summary (dict[tuple[int, int], BaselineFlagSummary]): Collection of flagging statistics accumulated when flagging
|
|
158
|
+
min_horizon_lim (u.Quantity): The minimum horizon limit applied to the flagging.
|
|
159
|
+
max_horizon_lim (u.Quantity): The maximum horizon limit applied to the flagging.
|
|
160
|
+
min_sun_scale (u.Quantity): The sun scale used to compute the uv-distance limiter.
|
|
161
|
+
dry_run (bool, optional): Indicates whether the flags were applied. Defaults to False.
|
|
162
|
+
|
|
163
|
+
"""
|
|
164
|
+
logger.info("----------------------------------")
|
|
165
|
+
logger.info("Flagging summary of modified flags")
|
|
166
|
+
logger.info(f"Minimum Horizon Limit: {min_horizon_lim}")
|
|
167
|
+
logger.info(f"Maximum Horizon Limit: {max_horizon_lim}")
|
|
168
|
+
logger.info(f"Minimum Sun Scale: {min_sun_scale}")
|
|
169
|
+
if dry_run:
|
|
170
|
+
logger.info("(Dry run, not applying)")
|
|
171
|
+
logger.info("----------------------------------")
|
|
172
|
+
|
|
173
|
+
for ants, baseline_summary in summary.items():
|
|
174
|
+
logger.info(
|
|
175
|
+
f"({ants[0]:3d},{ants[1]:3d}): uvw {baseline_summary.uvw_flag_perc:>6.2f}% & elev. {baseline_summary.elevation_flag_perc:>6.2f}% = Applied {baseline_summary.jolly_flag_perc:>6.2f}%"
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
logger.info("\n")
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def uvw_flagger(
|
|
182
|
+
computed_uvws: UVWs,
|
|
183
|
+
min_horizon_lim: u.Quantity = -3 * u.deg,
|
|
184
|
+
max_horizon_lim: u.Quantity = 90 * u.deg,
|
|
185
|
+
min_sun_scale: u.Quantity = 0.075 * u.deg,
|
|
186
|
+
dry_run: bool = False,
|
|
187
|
+
) -> Path:
|
|
188
|
+
"""Flag visibilities based on the (u, v, w)'s and assumed scales of
|
|
189
|
+
the sun. The routine will compute ht ebaseline length affected by the Sun
|
|
190
|
+
and then flagged visibilities where the projected (u,v)-distance towards
|
|
191
|
+
the direction of the Sun and presumably sensitive.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
computed_uvws (UVWs): The pre-computed UVWs and associated meta-data
|
|
195
|
+
min_horizon_lim (u.Quantity, optional): The lower horixzon limit required for flagging to be applied. Defaults to -3*u.deg.
|
|
196
|
+
max_horizon_lim (u.Quantity, optional): The upper horixzon limit required for flagging to be applied. Defaults to 90*u.deg.
|
|
197
|
+
min_sun_scale (u.Quantity, options): The minimum angular scale to consider when flagging the projected baselines. Defaults to 0.075*u.deg.
|
|
198
|
+
dry_run (bool, optional): Do not apply the flags to the measurement set. Defaults to False.
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
Path: The path to the flagged measurement set
|
|
203
|
+
"""
|
|
204
|
+
hour_angles = computed_uvws.hour_angles
|
|
205
|
+
baselines = computed_uvws.baselines
|
|
206
|
+
ms_path = computed_uvws.baselines.ms_path
|
|
207
|
+
|
|
208
|
+
sun_scale = get_sun_uv_scales(
|
|
209
|
+
ms_path=ms_path,
|
|
210
|
+
min_scale=min_sun_scale,
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
# A list of (ant1, ant2) to baseline index
|
|
214
|
+
antennas_for_baselines = baselines.b_map.keys()
|
|
215
|
+
logger.info(f"Will be considering {len(antennas_for_baselines)} baselines")
|
|
216
|
+
|
|
217
|
+
elevation_curve = hour_angles.elevation
|
|
218
|
+
|
|
219
|
+
# Used to capture the baseline and additional flags added
|
|
220
|
+
summary: dict[tuple[int, int], BaselineFlagSummary] = {}
|
|
221
|
+
|
|
222
|
+
logger.info(f"Opening {ms_path=}")
|
|
223
|
+
with table(str(ms_path), ack=False, readonly=False) as ms_tab:
|
|
224
|
+
for ant_1, ant_2 in tqdm(antennas_for_baselines):
|
|
225
|
+
logger.debug(f"Processing {ant_1=} {ant_2=}")
|
|
226
|
+
|
|
227
|
+
# Keeps the ruff from complaining about and unused varuable wheen
|
|
228
|
+
# it is used in the table access command below
|
|
229
|
+
_ = ms_tab
|
|
230
|
+
|
|
231
|
+
# TODO: It is unclear to TJG whether the time-order needs
|
|
232
|
+
# to be considered when reading in per-baseline at a time.
|
|
233
|
+
# initial version operated on a row basis so an explicit map
|
|
234
|
+
# to the t_idx of computed_uvws.uvws was needed (or useful?)
|
|
235
|
+
|
|
236
|
+
# Get the UVWs and for the baseline and calculate the uv-distance
|
|
237
|
+
b_idx = baselines.b_map[(ant_1, ant_2)]
|
|
238
|
+
uvws_bt = computed_uvws.uvws[:, b_idx]
|
|
239
|
+
uv_dist = np.sqrt((uvws_bt[0]) ** 2 + (uvws_bt[1]) ** 2).to(u.m).value
|
|
240
|
+
|
|
241
|
+
# The max angular scale corresponds to the shortest uv-distance
|
|
242
|
+
# The min angular scale corresponds to the longest uv-distance
|
|
243
|
+
flag_uv_dist = (
|
|
244
|
+
uv_dist[:, None]
|
|
245
|
+
<= sun_scale.min_scale_chan_lambda.to(u.m).value[None, :]
|
|
246
|
+
)
|
|
247
|
+
flag_elevation = (min_horizon_lim < elevation_curve)[:, None] & (
|
|
248
|
+
elevation_curve <= max_horizon_lim
|
|
249
|
+
)[:, None]
|
|
250
|
+
|
|
251
|
+
all_flags = flag_uv_dist & flag_elevation
|
|
252
|
+
|
|
253
|
+
# Only need to interact with the MS if there are flags to update
|
|
254
|
+
if not np.any(all_flags):
|
|
255
|
+
continue
|
|
256
|
+
|
|
257
|
+
baseline_summary = BaselineFlagSummary(
|
|
258
|
+
uvw_flag_perc=np.sum(flag_uv_dist)
|
|
259
|
+
/ np.prod(flag_uv_dist.shape)
|
|
260
|
+
* 100.0,
|
|
261
|
+
elevation_flag_perc=np.sum(flag_elevation)
|
|
262
|
+
/ np.prod(flag_elevation.shape)
|
|
263
|
+
* 100.0,
|
|
264
|
+
jolly_flag_perc=np.sum(all_flags) / np.prod(all_flags.shape) * 100.0,
|
|
265
|
+
)
|
|
266
|
+
summary[(ant_1, ant_2)] = baseline_summary
|
|
267
|
+
|
|
268
|
+
# Do not apply the flags mattteee
|
|
269
|
+
if dry_run:
|
|
270
|
+
continue
|
|
271
|
+
|
|
272
|
+
with taql(
|
|
273
|
+
"select from $ms_tab where ANTENNA1 == $ant_1 and ANTENNA2 == $ant_2",
|
|
274
|
+
) as subtab:
|
|
275
|
+
flags = subtab.getcol("FLAG")[:]
|
|
276
|
+
total_flags = flags | all_flags[..., None]
|
|
277
|
+
|
|
278
|
+
subtab.putcol("FLAG", total_flags)
|
|
279
|
+
subtab.flush()
|
|
280
|
+
|
|
281
|
+
log_summaries(
|
|
282
|
+
summary=summary,
|
|
283
|
+
min_horizon_lim=min_horizon_lim,
|
|
284
|
+
max_horizon_lim=max_horizon_lim,
|
|
285
|
+
min_sun_scale=min_sun_scale,
|
|
286
|
+
dry_run=dry_run,
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
return ms_path
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: jolly-roger
|
|
3
|
+
Version: 0.0.2
|
|
4
|
+
Summary: The pirate flagger
|
|
5
|
+
Project-URL: Homepage, https://github.com/flint-crew/jolly-roger
|
|
6
|
+
Project-URL: Bug Tracker, https://github.com/flint-crew/jolly-roger/issues
|
|
7
|
+
Project-URL: Discussions, https://github.com/flint-crew/jolly-roger/discussions
|
|
8
|
+
Project-URL: Changelog, https://github.com/flint-crew/jolly-roger/releases
|
|
9
|
+
Author-email: Alec Thomson <alec.thomson@csiro.au>
|
|
10
|
+
License-Expression: BSD-3-Clause
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Classifier: Development Status :: 1 - Planning
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Scientific/Engineering
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Python: >=3.11
|
|
25
|
+
Requires-Dist: astropy
|
|
26
|
+
Requires-Dist: numpy>=2.0.0
|
|
27
|
+
Requires-Dist: python-casacore>=3.6.0
|
|
28
|
+
Requires-Dist: tqdm
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# jolly-roger
|
|
32
|
+
|
|
33
|
+
[![Actions Status][actions-badge]][actions-link]
|
|
34
|
+
[![Documentation Status][rtd-badge]][rtd-link]
|
|
35
|
+
|
|
36
|
+
[![PyPI version][pypi-version]][pypi-link]
|
|
37
|
+
<!-- [![Conda-Forge][conda-badge]][conda-link] -->
|
|
38
|
+
[![PyPI platforms][pypi-platforms]][pypi-link]
|
|
39
|
+
|
|
40
|
+
<!-- [![GitHub Discussion][github-discussions-badge]][github-discussions-link] -->
|
|
41
|
+
|
|
42
|
+
<!-- SPHINX-START -->
|
|
43
|
+
|
|
44
|
+
<!-- prettier-ignore-start -->
|
|
45
|
+
[actions-badge]: https://github.com/flint-crew/jolly-roger/workflows/CI/badge.svg
|
|
46
|
+
[actions-link]: https://github.com/flint-crew/jolly-roger/actions
|
|
47
|
+
[conda-badge]: https://img.shields.io/conda/vn/conda-forge/jolly-roger
|
|
48
|
+
[conda-link]: https://github.com/conda-forge/jolly-roger-feedstock
|
|
49
|
+
[github-discussions-badge]: https://img.shields.io/static/v1?label=Discussions&message=Ask&color=blue&logo=github
|
|
50
|
+
[github-discussions-link]: https://github.com/flint-crew/jolly-roger/discussions
|
|
51
|
+
[pypi-link]: https://pypi.org/project/jolly-roger/
|
|
52
|
+
[pypi-platforms]: https://img.shields.io/pypi/pyversions/jolly-roger
|
|
53
|
+
[pypi-version]: https://img.shields.io/pypi/v/jolly-roger
|
|
54
|
+
[rtd-badge]: https://readthedocs.org/projects/jolly-roger/badge/?version=latest
|
|
55
|
+
[rtd-link]: https://jolly-roger.readthedocs.io/en/latest/?badge=latest
|
|
56
|
+
|
|
57
|
+
<!-- prettier-ignore-end -->
|
|
58
|
+
|
|
59
|
+
The pirate flagger!
|
|
60
|
+
|
|
61
|
+
# Installation
|
|
62
|
+
|
|
63
|
+
`pip install jolly-roger`
|
|
64
|
+
|
|
65
|
+
# About
|
|
66
|
+
|
|
67
|
+
This package attempts to flag visibilities that are contaminated by the Sun and uses a fairly simple heuristic based on the geometry of an interferometer.
|
|
68
|
+
|
|
69
|
+
In short, `jolly_roger` flags based on the projected baseline length should the array be tracking the Sun. The projected baseline length between some phased direction being tracked and the Sun can be significantly different. `jolly_roger` attempts to leverage this by only flagging data where the projected baseline length is between some nominal range that corresponds to angular scales associated with the Sun.
|
|
70
|
+
|
|
71
|
+
`jolly_roger` makes no guarentess about removing all contaminated visibilities, nor does it attempt to peel/subtract the Sun from the visibility data.
|
|
72
|
+
|
|
73
|
+
## How does it work?
|
|
74
|
+
|
|
75
|
+
`jolly_roger` will recompute the (u,v,w)-coordinates of a measurement set as if it were tracking the Sun, from which (u,v)-distances are derieved for each baseline and timestep. An updated `FLAG` column can then be inserted into the measurement set suppressing visibilities that would be sensitive to a nominated range of angular scales.
|
|
76
|
+
|
|
77
|
+
## Example
|
|
78
|
+
|
|
79
|
+
`jolly_roger` has a CLI entry point that can be called as:
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
jolly_flagger scienceData.EMU_1141-55.SB47138.EMU_1141-55.beam00_averaged_cal.leakage.ms --min-horizon-limit-deg '-2' --max-horizon-limit-deg 30 --min-scale-deg 0.075
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Here we are flagging visibilities that correspond to instances where:
|
|
86
|
+
- the Sun has an elevation between -2 and 30 degrees, and
|
|
87
|
+
- they are sensitive to angular scales between 0.075 and 1.0 segrees.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
jolly_roger/__init__.py,sha256=7xiZLdeY-7sgrYGQ1gNdCjgCfqnoPXK7AeaHncY_DGU,204
|
|
2
|
+
jolly_roger/_version.py,sha256=wO7XWlZte1hxA4mMvRc6zhNdGm74Nhhn2bfWRAxaKbI,511
|
|
3
|
+
jolly_roger/_version.pyi,sha256=j5kbzfm6lOn8BzASXWjGIA1yT0OlHTWqlbyZ8Si_o0E,118
|
|
4
|
+
jolly_roger/baselines.py,sha256=C_vC3v_ciU2T_si31oS0hUmsMNTQA0USxrm4118vYvY,4615
|
|
5
|
+
jolly_roger/flagger.py,sha256=tlC-M_MpLpqOvkF544zw2EvOUpbSpasO2zlMlXMcxSs,3034
|
|
6
|
+
jolly_roger/hour_angles.py,sha256=ChWTy69dkRN0R2HWEknHagv6W3xTXuSJJn9sAqBABCc,6160
|
|
7
|
+
jolly_roger/logging.py,sha256=04YVHnF_8tKDkXNtXQ-iMyJ2BLV-qowbPAqqMFDxYE4,1338
|
|
8
|
+
jolly_roger/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
+
jolly_roger/uvws.py,sha256=8G2E7bq8y5EEc2wQW5nli1WKsebZbBEHN8fhPzlinWk,10558
|
|
10
|
+
jolly_roger-0.0.2.dist-info/METADATA,sha256=BtHEC57mNeu12PI6_pnDSkD8i767av0kQfTr_vINWuI,4172
|
|
11
|
+
jolly_roger-0.0.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
12
|
+
jolly_roger-0.0.2.dist-info/entry_points.txt,sha256=ZwEZAe4DBn5nznVI0tP0a1wUinYouwXxxcZP6p7Pkvk,58
|
|
13
|
+
jolly_roger-0.0.2.dist-info/licenses/LICENSE,sha256=7G-TthaPSOehr-pdj4TJydXj3eIUmerMbCUSatMr8hc,1522
|
|
14
|
+
jolly_roger-0.0.2.dist-info/RECORD,,
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025, Alec Thomson.
|
|
4
|
+
All rights reserved.
|
|
5
|
+
|
|
6
|
+
Redistribution and use in source and binary forms, with or without
|
|
7
|
+
modification, are permitted provided that the following conditions are met:
|
|
8
|
+
|
|
9
|
+
* Redistributions of source code must retain the above copyright notice, this
|
|
10
|
+
list of conditions and the following disclaimer.
|
|
11
|
+
|
|
12
|
+
* Redistributions in binary form must reproduce the above copyright notice,
|
|
13
|
+
this list of conditions and the following disclaimer in the documentation
|
|
14
|
+
and/or other materials provided with the distribution.
|
|
15
|
+
|
|
16
|
+
* Neither the name of the vector package developers nor the names of its
|
|
17
|
+
contributors may be used to endorse or promote products derived from
|
|
18
|
+
this software without specific prior written permission.
|
|
19
|
+
|
|
20
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
21
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
22
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
23
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
24
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
25
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
26
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
27
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
28
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
29
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|