spotless 0.7.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.
spotless/__init__.py ADDED
@@ -0,0 +1,8 @@
1
+ #
2
+ # Copyright Tim Molteno 2017 tim@elec.ac.nz
3
+ #
4
+ # Init for the spotless imaging algorithm
5
+
6
+ from .model import Model
7
+ from .source import PointSource
8
+ from .spotless import Spotless
@@ -0,0 +1,188 @@
1
+ #!/usr/bin/env python
2
+ import matplotlib
3
+ import os
4
+ if os.name == 'posix' and "DISPLAY" not in os.environ:
5
+ matplotlib.use('Agg')
6
+ import matplotlib.pyplot as plt
7
+
8
+ import argparse
9
+ import json
10
+ import logging
11
+ from copy import deepcopy
12
+
13
+ import numpy as np
14
+
15
+ from tart.operation import settings
16
+
17
+ from tart_tools import api_handler
18
+ from tart_tools import api_imaging
19
+ from tart.imaging import elaz
20
+
21
+ from .spotless import Spotless
22
+ from .spotless import get_source_list
23
+
24
+
25
+ logger = logging.getLogger()
26
+
27
+
28
+ def handle_image(args, img, title, time_repr, src_list=None):
29
+ """ This function manages the output of an image, drawing sources e.t.c."""
30
+ image_title = '{}_{}'.format(title, time_repr)
31
+ plt.title(image_title)
32
+ if args.fits:
33
+ fname = '{}.fits'.format(image_title)
34
+ fpath = os.path.join(args.dir, fname)
35
+ api_imaging.save_fits_image(img, fname=fname, out_dir=args.dir, timestamp=time_repr)
36
+ logger.info("Generating {}".format(fname))
37
+ if args.PNG:
38
+ fname = '{}.png'.format(image_title)
39
+ fpath = os.path.join(args.dir, fname)
40
+ plt.savefig(fpath, dpi=300)
41
+ logger.info("Generating {}".format(fname))
42
+ if args.PDF:
43
+ fname = '{}.pdf'.format(image_title)
44
+ fpath = os.path.join(args.dir, fname)
45
+ plt.savefig(fpath, dpi=600)
46
+ logger.info("Generating {}".format(fname))
47
+ if args.display:
48
+ plt.show()
49
+
50
+
51
+ def main():
52
+ parser = argparse.ArgumentParser(
53
+ description='Generate an all-sky TART Gridless Image',
54
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter)
55
+
56
+ parser.add_argument(
57
+ '--api', required=False,
58
+ default='https://tart.elec.ac.nz/signal',
59
+ help="Telescope API server URL.")
60
+
61
+ parser.add_argument('--catalog', required=False, default='https://tart.elec.ac.nz/catalog', help="Catalog API URL.")
62
+
63
+ parser.add_argument('--file', required=False, default=None, help="Snapshot ovservation saved JSON file (visiblities, positions and more).")
64
+ parser.add_argument('--vis', required=False, default=None, help="Use a local JSON file containing the visibilities to create the image.")
65
+ parser.add_argument('--dir', required=False, default='.', help="Output directory.")
66
+ parser.add_argument('--rotation', type=float, default=0.0, help="Apply rotation (in degrees) to the antenna positions.")
67
+ parser.add_argument('--nside', type=int, default=8, help="Healpix nside parameter for display purposes only.")
68
+
69
+ parser.add_argument('--beam', action="store_true", help="Generate a gridless beam.")
70
+
71
+ parser.add_argument('--elevation', type=float, default=20.0, help="Elevation limit for displaying sources (degrees)")
72
+ parser.add_argument('--display', action="store_true", help="Display Image to the user")
73
+ parser.add_argument('--fits', action="store_true", help="Generate a FITS format image")
74
+ parser.add_argument('--PNG', action="store_true", help="Generate a PNG format image")
75
+ parser.add_argument('--PDF', action="store_true", help="Generate a PDF format image")
76
+ parser.add_argument('--show-sources', action="store_true", help="Show known sources on images (only works on PNG).")
77
+
78
+ parser.add_argument('--title', required=False, default="", help="Prefix the title.")
79
+
80
+ parser.add_argument('--version', action="store_true",
81
+ help="Display the current version")
82
+ parser.add_argument('--debug', action="store_true",
83
+ help="Display debugging information")
84
+
85
+ source_json = None
86
+
87
+ ARGS = parser.parse_args()
88
+
89
+ if ARGS.debug:
90
+ level = logging.DEBUG
91
+ else:
92
+ level = logging.ERROR
93
+
94
+ logger = logging.getLogger('spotless')
95
+ logger.setLevel(level)
96
+
97
+ if ARGS.debug:
98
+ fh = logging.FileHandler('spotless.log')
99
+ fh.setLevel(level)
100
+
101
+ # create console handler and set level to debug
102
+ ch = logging.StreamHandler()
103
+ ch.setLevel(level)
104
+
105
+ # create formatter
106
+ formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
107
+
108
+ # add formatter to ch
109
+ ch.setFormatter(formatter)
110
+ fh.setFormatter(formatter)
111
+
112
+ # add ch to logger
113
+ logger.addHandler(ch)
114
+
115
+ if ARGS.version:
116
+ version = version("spotless")
117
+ print(f"gridless: Version {version}")
118
+ print(" (c) 2022-2023 Tim Molteno")
119
+ sys.exit(0)
120
+
121
+
122
+ if ARGS.file:
123
+ logger.info("Getting Data from file: {}".format(ARGS.file))
124
+ # Load data from a JSON file
125
+ with open(ARGS.file, 'r') as json_file:
126
+ calib_info = json.load(json_file)
127
+
128
+ info = calib_info['info']
129
+ ant_pos = calib_info['ant_pos']
130
+ config = settings.from_api_json(info['info'], ant_pos)
131
+
132
+ flag_list = [] # [4, 5, 14, 22]
133
+
134
+ original_positions = deepcopy(config.get_antenna_positions())
135
+
136
+ gains_json = calib_info['gains']
137
+ gains = np.asarray(gains_json['gain'])
138
+ phase_offsets = np.asarray(gains_json['phase_offset'])
139
+ config = settings.from_api_json(info['info'], ant_pos)
140
+
141
+ measurements = []
142
+ for d in calib_info['data']:
143
+ vis_json, source_json = d
144
+ cv, timestamp = api_imaging.vis_calibrated(vis_json, config, gains, phase_offsets, flag_list)
145
+ src_list = elaz.from_json(source_json, 0.0)
146
+ if not ARGS.show_sources:
147
+ src_list = None
148
+ else:
149
+ logger.info("Getting Data from API: {}".format(ARGS.api))
150
+
151
+ api = api_handler.APIhandler(ARGS.api)
152
+ config = api_handler.get_config(api)
153
+
154
+ gains = api.get('calibration/gain')
155
+
156
+ if (ARGS.vis is None):
157
+ vis_json = api.get('imaging/vis')
158
+ else:
159
+ with open(ARGS.vis, 'r') as json_file:
160
+ vis_json = json.load(json_file)
161
+
162
+ ts = api_imaging.vis_json_timestamp(vis_json)
163
+ if ARGS.show_sources:
164
+ source_json = api.get_url(api.catalog_url(config, datestr=ts.isoformat()))
165
+
166
+ logger.info("Data Download Complete")
167
+
168
+ cv, timestamp = api_imaging.vis_calibrated(vis_json, config, gains['gain'], gains['phase_offset'], flag_list=[])
169
+
170
+ api_imaging.rotate_vis(ARGS.rotation, cv, reference_positions = deepcopy(config.get_antenna_positions()))
171
+
172
+ time_repr = "{:%Y_%m_%d_%H_%M_%S_%Z}".format(timestamp)
173
+
174
+ # Processing
175
+ should_make_images = ARGS.display or ARGS.PNG or ARGS.fits
176
+
177
+ spot = Spotless(cv)
178
+
179
+ src_list = get_source_list(source_json, el_limit=ARGS.elevation, jy_limit=1e4)
180
+
181
+ if should_make_images:
182
+ nside = 2**ARGS.nside
183
+ spt = spot.display(plt, src_list, nside, False)
184
+ handle_image(ARGS, None, "" + ARGS.title, time_repr, src_list)
185
+
186
+ if ARGS.beam:
187
+ spot.beam(plt, nside)
188
+ handle_image(ARGS, None, "dirty", "beam", src_list)
spotless/model.py ADDED
@@ -0,0 +1,74 @@
1
+ #
2
+ # Copyright Tim Molteno 2017-2022 tim@elec.ac.nz
3
+ # License GPLv3
4
+ #
5
+
6
+ import numpy as np
7
+ import json
8
+
9
+ from .source import PointSource
10
+
11
+
12
+ class Model(object):
13
+
14
+ def __init__(self):
15
+ self.objects = []
16
+
17
+ def add_source(self, src):
18
+ self.objects.append(src)
19
+
20
+ def __getitem__(self, index):
21
+ return self.objects[index]
22
+
23
+ def model_vis(self, u_arr, v_arr, w_arr):
24
+ vis = np.zeros_like(u_arr, dtype=np.complex64)
25
+
26
+ for src in self.objects:
27
+ vis += src.get_vis(u_arr, v_arr, w_arr)
28
+ return vis
29
+
30
+ def to_dict(self):
31
+ ret = {}
32
+ objs = [obj.to_dict() for obj in self.objects]
33
+ ret["model"] = objs
34
+ return ret
35
+
36
+ def __repr__(self):
37
+ return json.dumps(self.to_dict(), indent=4)
38
+
39
+ def brightest(self):
40
+ '''Return Brightest Source'''
41
+ ret = None
42
+ b = 0.0
43
+ for src in self.objects:
44
+ if src.a > b:
45
+ b = src.a
46
+ ret = src
47
+ return ret
48
+
49
+ def faintest(self):
50
+ '''Return Faintest Source'''
51
+ ret = None
52
+ b = 9e99
53
+ for src in self.objects:
54
+ if src.a < b:
55
+ b = src.a
56
+ ret = src
57
+ return ret
58
+
59
+ def to_vector(self):
60
+ ret = []
61
+ for src in self.objects:
62
+ ret.append(src.a)
63
+ ret.append(src.el)
64
+ ret.append(src.az)
65
+ return np.array(ret)
66
+
67
+ @classmethod
68
+ def from_vector(cls, vect):
69
+ ret = cls()
70
+ bits = np.split(vect, len(vect)/3)
71
+ for src in bits:
72
+ a, el, az = src
73
+ ret.add_source(PointSource(a, el, az))
74
+ return ret
@@ -0,0 +1,82 @@
1
+ #
2
+ # Copyright Tim Molteno 2017-2022 tim@elec.ac.nz
3
+ # License GPLv3
4
+ #
5
+
6
+ import logging
7
+
8
+ import numpy as np
9
+ from scipy.optimize import minimize
10
+
11
+ from .model import Model
12
+ from .source import PointSource
13
+ from .spotless import Spotless
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class MultiSpotless(Spotless):
19
+ def __init__(self, disko, sphere):
20
+ super(MultiSpotless, self).__init__(disko, sphere)
21
+
22
+ def step(self):
23
+ """
24
+ Return a list of residual visibilities and a source location
25
+ so that the sum of the residual and the source visibilities
26
+ is conserved.
27
+ """
28
+ d_el = self.disko.get_beam_width().radians() / 2
29
+
30
+ m_vis = self.model.model_vis(
31
+ self.disko.u_arr, self.disko.v_arr, self.disko.w_arr
32
+ )
33
+
34
+ a_0, el_0, az_0, p0 = self.estimate_initial_point_source(self.vis_arr - m_vis)
35
+
36
+ a_init = max(a_0, 0.01)
37
+ model_vect = self.model.to_vector()
38
+ x0 = np.append(model_vect, [a_init, el_0, az_0])
39
+
40
+ # Get the bounds of the existing points. Each dimension is
41
+ # constrained appropriately.
42
+ bounds = []
43
+ for src in self.model:
44
+ for b in src.get_bounds(d_el, self.el_threshold_r):
45
+ bounds.append(b)
46
+
47
+ src = PointSource(a_init, el_0, az_0)
48
+ for b in src.get_bounds(d_el, self.el_threshold_r):
49
+ bounds.append(b)
50
+
51
+ # Use Nelder-Mead for MultiSpotless: the mixed amplitude/angle
52
+ # parameter space is poorly scaled, making gradient-based
53
+ # methods (L-BFGS-B) unreliable. Nelder-Mead only uses function
54
+ # values and handles this naturally.
55
+ fmin = minimize(
56
+ self.f_n,
57
+ x0.flatten(),
58
+ method="Nelder-Mead",
59
+ bounds=bounds,
60
+ options={
61
+ "xatol": 1e-3,
62
+ "fatol": 0.1,
63
+ "adaptive": True,
64
+ },
65
+ )
66
+ residual_power = fmin.fun
67
+
68
+ self.model = Model.from_vector(fmin.x)
69
+
70
+ self.residual_vis = self.vis_arr - self.model.model_vis(
71
+ self.disko.u_arr, self.disko.v_arr, self.disko.w_arr
72
+ )
73
+
74
+ return self.model, residual_power, p0
75
+
76
+ def f_n(self, x):
77
+ """
78
+ Find the power in the residual
79
+ """
80
+ mod = Model.from_vector(x)
81
+ m_vis = mod.model_vis(self.disko.u_arr, self.disko.v_arr, self.disko.w_arr)
82
+ return self.power(self.vis_arr - m_vis)
spotless/source.py ADDED
@@ -0,0 +1,85 @@
1
+ #
2
+ # Copyright Tim Molteno 2017-2022 tim@elec.ac.nz
3
+ # License GPLv3
4
+ #
5
+
6
+ import logging
7
+ from disko import jomega
8
+
9
+ import numpy as np
10
+ import json
11
+ from . import sphere
12
+
13
+ from tart.util import constants
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class PointSource(object):
19
+ '''
20
+ A single point source in el,az co-ordinates
21
+ '''
22
+
23
+ def __init__(self, a, el, az):
24
+ self.a = a
25
+ self.el = el
26
+ self.az = az
27
+ self.power = None # The power will be set after fitting
28
+
29
+ def get_power(self):
30
+ if self.power is None:
31
+ return self.a
32
+ return self.power
33
+
34
+ def to_dict(self):
35
+ ret = {}
36
+ ret["a"] = self.a
37
+ ret["el"] = np.degrees(self.el)
38
+ ret["az"] = np.degrees(self.az)
39
+ if (self.power is not None):
40
+ ret["p"] = float(self.power)
41
+ # return "{p:None, a:{:03.2f}, el:{:03.1f}, az:{:03.1f}},".format(self.a, np.degrees(self.el), np.degrees(self.az))
42
+ # else:
43
+ # return "{p:{:03.2f}, a:{:03.2f}, el:{:03.1f}, az:{:03.1f}},".format(self.power,
44
+ # self.a, np.degrees(self.el), np.degrees(self.az))
45
+ return ret
46
+
47
+ def __repr__(self):
48
+ return json.dumps(self.to_dict())
49
+
50
+ def get_vis(self, u_arr, v_arr, w_arr):
51
+ '''
52
+ Generate a list of visibilities from the source at u,v,w points
53
+ See "The non-coplanar baselines effect in radio interferometry: The W-projection algorithm"
54
+ by Tim Cornwell. Note that this paper chooses direction cosines (l,m) as its basis for the
55
+ integral and the image coordinates. It introduces a factor sqrt(1 - l**2 - m**2) which
56
+ implies that visibilities are infinite when the object is on the horizon (l or m ==1). This
57
+ is really just a correction because dl dm approaches zero at this point.
58
+
59
+ The Smirnov papers on the RIME provide a formalism that is easier to derive algorithms for.
60
+
61
+ TODO: Use an antenna model (for the whole telescope initially, and then eventually for each antenna)
62
+ to modify the source amplitude (self.a) and predict the visibilities from a low-elevation source.
63
+ '''
64
+ l, m, n = sphere.elaz2lmn(self.el, self.az)
65
+
66
+ p2j = jomega(constants.L1_FREQ)
67
+ vis = self.a*self.a * \
68
+ np.exp(-p2j*(u_arr*l + v_arr*m + w_arr*(n - 1.0)))
69
+ return vis
70
+
71
+ def get_bounds(self, d_el, el_threshold_r=0):
72
+ '''
73
+ TODO check that the source remains inside the sphere.
74
+ '''
75
+ logger.info(f" get_bounds({d_el}, {el_threshold_r})")
76
+ d_az = np.abs(d_el/(np.cos(self.el) + 0.001))
77
+
78
+ el_lower = max(self.el - d_el, el_threshold_r)
79
+ el_upper = max(el_threshold_r, self.el + d_el)
80
+
81
+ logger.info(f" Elevation: ({el_lower}, {el_upper})")
82
+
83
+ return [(0.0, None),
84
+ (el_lower, min(el_upper, np.pi/2)),
85
+ (self.az - d_az, self.az + d_az)]
spotless/sphere.py ADDED
@@ -0,0 +1,28 @@
1
+ #
2
+ # Copyright Tim Molteno 2017-2022 tim@elec.ac.nz
3
+ # License GPLv3
4
+ #
5
+ # Classes to hold pixelated spheres
6
+ #
7
+
8
+ import logging
9
+ import numpy as np
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ def elaz2lmn(el_r, az_r):
15
+ ll = np.sin(az_r)*np.cos(el_r)
16
+ m = np.cos(az_r)*np.cos(el_r)
17
+ # Often written in this weird way... np.sqrt(1.0 - l**2 - m**2)
18
+ n = np.sin(el_r)
19
+ return ll, m, n
20
+
21
+
22
+ def get_peak(sphere):
23
+ i = np.argmax(sphere.pixels)
24
+ a_0 = sphere.pixels[i]
25
+ el_0 = sphere.el_r[i]
26
+ az_0 = sphere.az_r[i]
27
+
28
+ return a_0, el_0, az_0