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 +8 -0
- spotless/gridless_cli.py +188 -0
- spotless/model.py +74 -0
- spotless/multi_spotless.py +82 -0
- spotless/source.py +85 -0
- spotless/sphere.py +28 -0
- spotless/spotless.py +376 -0
- spotless/spotless_calibrate_cli.py +260 -0
- spotless/spotless_cli.py +391 -0
- spotless/tests/__init__.py +0 -0
- spotless/tests/test_model.py +37 -0
- spotless/tests/test_source.py +21 -0
- spotless/tests/test_spotless.py +164 -0
- spotless-0.7.2.dist-info/METADATA +125 -0
- spotless-0.7.2.dist-info/RECORD +18 -0
- spotless-0.7.2.dist-info/WHEEL +4 -0
- spotless-0.7.2.dist-info/entry_points.txt +4 -0
- spotless-0.7.2.dist-info/licenses/LICENSE.txt +674 -0
spotless/__init__.py
ADDED
spotless/gridless_cli.py
ADDED
|
@@ -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
|